diff --git a/.env.example b/.env.example index 005102a..e68943e 100644 --- a/.env.example +++ b/.env.example @@ -9,6 +9,8 @@ CRON_WEB_LIMIT=5 DB_CONNECTION=mysql DB_HOST=127.0.0.1 +# Tylko techniczne operacje agenta (np. reczne migracje), nie dla runtime aplikacji. +DB_HOST_REMOTE= DB_PORT=3306 DB_DATABASE=orderpro DB_USERNAME=root diff --git a/.vscode/ftp-kr.sync.cache.json b/.vscode/ftp-kr.sync.cache.json index f9d720a..9ac54f1 100644 --- a/.vscode/ftp-kr.sync.cache.json +++ b/.vscode/ftp-kr.sync.cache.json @@ -3,10 +3,368 @@ "public_html": { "AGENTS.md": { "type": "-", - "size": 1105, - "lmtime": 1772398011560, + "size": 2207, + "lmtime": 1772497458624, "modified": false }, + "ARCHITECTURE.md": { + "type": "-", + "size": 659, + "lmtime": 1772490697553, + "modified": false + }, + "archive": { + "2026-03-02_users-only-reset": { + "resources": { + "views": { + "dashboard": { + "index.php": { + "type": "-", + "size": 315, + "lmtime": 1771866989000, + "modified": false + } + }, + "marketplace": { + "index.php": { + "type": "-", + "size": 1669, + "lmtime": 1771922314000, + "modified": false + }, + "offers.php": { + "type": "-", + "size": 19158, + "lmtime": 1772397952604, + "modified": false + } + }, + "orders": { + "index.php": { + "type": "-", + "size": 710, + "lmtime": 1772490033013, + "modified": false + } + }, + "products": { + "create.php": { + "type": "-", + "size": 7204, + "lmtime": 1771868875000, + "modified": false + }, + "edit.php": { + "type": "-", + "size": 27948, + "lmtime": 1772397133535, + "modified": false + }, + "index.php": { + "type": "-", + "size": 11064, + "lmtime": 1771956268000, + "modified": false + }, + "links.php": { + "type": "-", + "size": 13765, + "lmtime": 1771954576000, + "modified": false + }, + "show.php": { + "type": "-", + "size": 9854, + "lmtime": 1772220108000, + "modified": false + } + }, + "settings": { + "cron.php": { + "type": "-", + "size": 7180, + "lmtime": 1772485558106, + "modified": false + }, + "database.php": { + "type": "-", + "size": 4478, + "lmtime": 1772485529509, + "modified": false + }, + "gs1.php": { + "type": "-", + "size": 3499, + "lmtime": 1772485576494, + "modified": false + }, + "integrations.php": { + "type": "-", + "size": 11056, + "lmtime": 1772488994330, + "modified": false + }, + "order-statuses.php": { + "type": "-", + "size": 5566, + "lmtime": 1772485520769, + "modified": false + }, + "products.php": { + "type": "-", + "size": 2225, + "lmtime": 1772485593115, + "modified": false + } + } + } + }, + "src": { + "Modules": { + "Cron": { + "CronJobProcessor.php": { + "type": "-", + "size": 6385, + "lmtime": 1771954453000, + "modified": false + }, + "CronJobRepository.php": { + "type": "-", + "size": 17045, + "lmtime": 1771954938000, + "modified": false + }, + "CronJobType.php": { + "type": "-", + "size": 1231, + "lmtime": 1772489146286, + "modified": false + }, + "ProductLinksHealthCheckHandler.php": { + "type": "-", + "size": 5247, + "lmtime": 1771954535000, + "modified": false + }, + "ShopProOfferTitlesRefreshHandler.php": { + "type": "-", + "size": 3788, + "lmtime": 1772397918784, + "modified": false + }, + "ShopProOrdersImportHandler.php": { + "type": "-", + "size": 536, + "lmtime": 1772484067565, + "modified": false + }, + "ShopProOrderStatusSyncHandler.php": { + "type": "-", + "size": 528, + "lmtime": 1772489139382, + "modified": false + } + }, + "GS1": { + "GS1Service.php": { + "type": "-", + "size": 2412, + "lmtime": 1772132619000, + "modified": false + }, + "MojeGS1Client.php": { + "type": "-", + "size": 6727, + "lmtime": 1771961979000, + "modified": false + } + }, + "Marketplace": { + "MarketplaceController.php": { + "type": "-", + "size": 28819, + "lmtime": 1772398277623, + "modified": false + }, + "MarketplaceRepository.php": { + "type": "-", + "size": 10298, + "lmtime": 1772398268053, + "modified": false + } + }, + "Orders": { + "OrderImportService.php": { + "type": "-", + "size": 21009, + "lmtime": 1772490222940, + "modified": false + }, + "OrdersController.php": { + "type": "-", + "size": 35423, + "lmtime": 1772490255436, + "modified": false + }, + "OrdersRepository.php": { + "type": "-", + "size": 25665, + "lmtime": 1772489045864, + "modified": false + }, + "OrderStatusSyncService.php": { + "type": "-", + "size": 17295, + "lmtime": 1772489130897, + "modified": false + } + }, + "ProductLinks": { + "ChannelOffersRepository.php": { + "type": "-", + "size": 10755, + "lmtime": 1771954497000, + "modified": false + }, + "LinkMatcherService.php": { + "type": "-", + "size": 1893, + "lmtime": 1771882685000, + "modified": false + }, + "OfferImportService.php": { + "type": "-", + "size": 8091, + "lmtime": 1771954510000, + "modified": false + }, + "ProductLinksController.php": { + "type": "-", + "size": 5392, + "lmtime": 1771882733000, + "modified": false + }, + "ProductLinksRepository.php": { + "type": "-", + "size": 20901, + "lmtime": 1771954562000, + "modified": false + }, + "ProductLinksService.php": { + "type": "-", + "size": 14754, + "lmtime": 1771927037000, + "modified": false + } + }, + "Products": { + "ProductRepository.php": { + "type": "-", + "size": 29887, + "lmtime": 1772395707501, + "modified": false + }, + "ProductsController.php": { + "type": "-", + "size": 49058, + "lmtime": 1772395718310, + "modified": false + }, + "ProductService.php": { + "type": "-", + "size": 17193, + "lmtime": 1772395136766, + "modified": false + }, + "ProductSkuGenerator.php": { + "type": "-", + "size": 3044, + "lmtime": 1772395702627, + "modified": false + }, + "ProductValidator.php": { + "type": "-", + "size": 3675, + "lmtime": 1771868735000, + "modified": false + }, + "ShopProExportService.php": { + "type": "-", + "size": 45644, + "lmtime": 1772395159115, + "modified": false + } + }, + "Settings": { + "AppSettingsRepository.php": { + "type": "-", + "size": 1905, + "lmtime": 1771954924000, + "modified": false + }, + "IntegrationRepository.php": { + "type": "-", + "size": 25754, + "lmtime": 1772488971508, + "modified": false + }, + "OrderStatusMappingRepository.php": { + "type": "-", + "size": 4135, + "lmtime": 1772489019745, + "modified": false + }, + "SettingsController.php": { + "type": "-", + "size": 73812, + "lmtime": 1772488985859, + "modified": false + }, + "ShopProClient.php": { + "type": "-", + "size": 40035, + "lmtime": 1772490209403, + "modified": false + } + } + } + }, + "bin": { + "cron.php": { + "type": "-", + "size": 4062, + "lmtime": 1772489168039, + "modified": false + } + }, + "tests": { + "Unit": { + "Cron": { + "CronJobTypeTest.php": { + "type": "-", + "size": 603, + "lmtime": 1772489500486, + "modified": false + } + }, + "Settings": { + "OrderStatusMappingRepositoryTest.php": { + "type": "-", + "size": 2415, + "lmtime": 1772489512491, + "modified": false + }, + "ShopProClientTest.php": { + "type": "-", + "size": 972, + "lmtime": 1772489519995, + "modified": false + } + } + } + } + } + }, "bin": { "build-assets.php": { "type": "-", @@ -16,8 +374,20 @@ }, "cron.php": { "type": "-", - "size": 2953, - "lmtime": 1772397935851, + "size": 114, + "lmtime": 1772490484677, + "modified": false + }, + "deploy_and_seed_orders.php": { + "type": "-", + "size": 34248, + "lmtime": 1772497088256, + "modified": false + }, + "fill_order_item_images.php": { + "type": "-", + "size": 2210, + "lmtime": 1772495582001, "modified": false }, "fix_gs1_brand.php": { @@ -26,6 +396,12 @@ "lmtime": 1772132695646, "modified": false }, + "fix_status_codes.php": { + "type": "-", + "size": 6275, + "lmtime": 1772493487035, + "modified": false + }, "migrate.php": { "type": "-", "size": 1357, @@ -37,6 +413,12 @@ "size": 7348, "lmtime": 1771964550467, "modified": false + }, + "randomize_order_statuses.php": { + "type": "-", + "size": 3646, + "lmtime": 1772497235553, + "modified": false } }, "bootstrap": { @@ -56,8 +438,8 @@ }, "composer.json": { "type": "-", - "size": 413, - "lmtime": 1771954556361, + "size": 586, + "lmtime": 1772489482442, "modified": false }, "config": { @@ -183,95 +565,95 @@ "size": 535, "lmtime": 1772397943532, "modified": false + }, + "20260302_000017_add_shoppro_orders_fetch_settings_to_integrations.sql": { + "type": "-", + "size": 186, + "lmtime": 0, + "modified": false + }, + "20260302_000018_create_orders_tables_and_schedule.sql": { + "type": "-", + "size": 3464, + "lmtime": 0, + "modified": false + }, + "20260302_000019_add_internal_order_number_to_orders.sql": { + "type": "-", + "size": 305, + "lmtime": 0, + "modified": false + }, + "20260302_000020_create_order_status_mappings_table.sql": { + "type": "-", + "size": 889, + "lmtime": 0, + "modified": false + }, + "20260302_000021_add_order_status_sync_direction_and_schedule.sql": { + "type": "-", + "size": 1474, + "lmtime": 1772489009411, + "modified": false + }, + "20260302_000022_create_order_status_groups_and_statuses_tables.sql": { + "type": "-", + "size": 1378, + "lmtime": 1772491798937, + "modified": false } }, - "seeders": {} + "seeders": {}, + "drafts": { + "20260302_orders_schema_apilo_v1.sql": { + "type": "-", + "size": 11439, + "lmtime": 1772493909912, + "modified": false + }, + "20260302_orders_schema_v1.sql": { + "type": "-", + "size": 10866, + "lmtime": 1772494371620, + "modified": false + } + } + }, + "DB_SCHEMA.md": { + "type": "-", + "size": 363, + "lmtime": 1772490689218, + "modified": false }, "DOCS": { - "API.md": { + "ARCHITECTURE.md": { "type": "-", - "size": 12521, - "lmtime": 0, - "modified": false - }, - "BACKLOG_MIKROZADANIA.md": { - "type": "-", - "size": 5872, - "lmtime": 1771869108326, - "modified": false - }, - "CRON_QUEUE.md": { - "type": "-", - "size": 1192, - "lmtime": 1771955129456, - "modified": false - }, - "FRONTEND_STANDARDS.md": { - "type": "-", - "size": 766, - "lmtime": 1771691123295, - "modified": false - }, - "MIGRATIONS.md": { - "type": "-", - "size": 599, - "lmtime": 1771692333726, - "modified": false - }, - "PLAN_MODULU_POWIAZAN_PRODUKTOW.md": { - "type": "-", - "size": 9167, - "lmtime": 1771883209024, - "modified": false - }, - "PLAN_MODULU_PRODUKTOW.md": { - "type": "-", - "size": 9624, - "lmtime": 1771868040816, - "modified": false - }, - "PLAN_PROJEKTU.md": { - "type": "-", - "size": 5181, - "lmtime": 1771460804927, - "modified": false - }, - "plans": { - "2026-02-27-marketplace-category-assignment-design.md": { - "type": "-", - "size": 3711, - "lmtime": 1772214308359, - "modified": false - }, - "2026-02-27-marketplace-category-assignment.md": { - "type": "-", - "size": 31532, - "lmtime": 1772214545805, - "modified": false - }, - "2026-02-27-per-integration-product-content-design.md": { - "type": "-", - "size": 3409, - "lmtime": 1772211731678, - "modified": false - }, - "2026-02-27-per-integration-product-content.md": { - "type": "-", - "size": 21040, - "lmtime": 1772211846717, - "modified": false - } - }, - "TODO.md": { - "type": "-", - "size": 395, - "lmtime": 1772395982288, + "size": 7199, + "lmtime": 1772497590235, "modified": false }, "DB_SCHEMA.md": { "type": "-", - "size": 4702, - "lmtime": 1772398004815, + "size": 3138, + "lmtime": 1772496281398, + "modified": false + }, + "ORDERS_SCHEMA_APILO_DRAFT.md": { + "type": "-", + "size": 2464, + "lmtime": 1772493922430, + "modified": false + }, + "ORDERS_SCHEMA_DRAFT.md": { + "type": "-", + "size": 1706, + "lmtime": 1772494183695, + "modified": false + }, + "TECH_CHANGELOG.md": { + "type": "-", + "size": 8638, + "lmtime": 1772497584302, "modified": false } }, @@ -289,8 +671,8 @@ }, ".env.example": { "type": "-", - "size": 321, - "lmtime": 1771955059028, + "size": 422, + "lmtime": 1772491020678, "modified": false }, ".gitignore": { @@ -1483,13 +1865,19 @@ "lmtime": 1771869056394, "modified": false }, + "phpunit.xml": { + "type": "-", + "size": 480, + "lmtime": 1772489488633, + "modified": false + }, "public": { "assets": { "css": { "app.css": { "type": "-", - "size": 15404, - "lmtime": 1772212909926, + "size": 25538, + "lmtime": 1772497577480, "modified": false }, "app.css.map": { @@ -1501,7 +1889,7 @@ "login.css": { "type": "-", "size": 4665, - "lmtime": 1772212910423, + "lmtime": 1772497577979, "modified": false }, "login.css.map": { @@ -1549,8 +1937,8 @@ "lang": { "pl.php": { "type": "-", - "size": 24349, - "lmtime": 1772397947563, + "size": 33450, + "lmtime": 1772496207146, "modified": false } }, @@ -1573,8 +1961,8 @@ "scss": { "app.scss": { "type": "-", - "size": 16579, - "lmtime": 1772212895245, + "size": 28671, + "lmtime": 1772497569553, "modified": false }, "login.scss": { @@ -1586,8 +1974,8 @@ "shared": { "_ui-components.scss": { "type": "-", - "size": 3436, - "lmtime": 1772210480823, + "size": 3420, + "lmtime": 1772493048183, "modified": false } } @@ -1607,6 +1995,12 @@ "size": 21805, "lmtime": 1771925480312, "modified": false + }, + "order-status-panel.php": { + "type": "-", + "size": 1743, + "lmtime": 1772497361423, + "modified": false } }, "dashboard": { @@ -1620,8 +2014,8 @@ "layouts": { "app.php": { "type": "-", - "size": 4012, - "lmtime": 1772220106049, + "size": 3524, + "lmtime": 1772495209961, "modified": false }, "auth.php": { @@ -1645,6 +2039,26 @@ "modified": false } }, + "orders": { + "index.php": { + "type": "-", + "size": 710, + "lmtime": 1772490033013, + "modified": false + }, + "list.php": { + "type": "-", + "size": 3035, + "lmtime": 1772497374098, + "modified": false + }, + "show.php": { + "type": "-", + "size": 12663, + "lmtime": 1772497562866, + "modified": false + } + }, "products": { "create.php": { "type": "-", @@ -1680,41 +2094,53 @@ "settings": { "cron.php": { "type": "-", - "size": 6992, + "size": 7180, "lmtime": 1772395792152, - "modified": false + "modified": true }, "database.php": { "type": "-", - "size": 4291, - "lmtime": 1772395778355, + "size": 3113, + "lmtime": 1772491513567, "modified": false }, "gs1.php": { "type": "-", - "size": 3312, + "size": 3499, "lmtime": 1772395799007, - "modified": false + "modified": true }, "integrations.php": { "type": "-", - "size": 9364, - "lmtime": 1772395786681, + "size": 11056, + "lmtime": 1772488994330, + "modified": false + }, + "order-statuses.php": { + "type": "-", + "size": 5566, + "lmtime": 0, "modified": false }, "products.php": { "type": "-", - "size": 2037, + "size": 2225, "lmtime": 1772395769190, + "modified": true + }, + "statuses.php": { + "type": "-", + "size": 17765, + "lmtime": 1772493011660, "modified": false } }, "users": { "index.php": { "type": "-", - "size": 1569, - "lmtime": 1771691646298, - "modified": true + "size": 1600, + "lmtime": 1772491504461, + "modified": false } } } @@ -1722,8 +2148,8 @@ "routes": { "web.php": { "type": "-", - "size": 10852, - "lmtime": 1772397082719, + "size": 3538, + "lmtime": 1772496154180, "modified": false } }, @@ -1771,8 +2197,8 @@ "Core": { "Application.php": { "type": "-", - "size": 9549, - "lmtime": 1772397927382, + "size": 7008, + "lmtime": 1772495113026, "modified": false }, "Database": { @@ -1866,8 +2292,8 @@ "Auth": { "AuthController.php": { "type": "-", - "size": 2470, - "lmtime": 1771460453479, + "size": 2543, + "lmtime": 1772491348696, "modified": false }, "AuthMiddleware.php": { @@ -1898,8 +2324,8 @@ }, "CronJobType.php": { "type": "-", - "size": 897, - "lmtime": 1772397902687, + "size": 1231, + "lmtime": 1772489146286, "modified": false }, "ProductLinksHealthCheckHandler.php": { @@ -1913,6 +2339,32 @@ "size": 3788, "lmtime": 1772397918784, "modified": false + }, + "ShopProOrdersImportHandler.php": { + "type": "-", + "size": 536, + "lmtime": 0, + "modified": false + }, + "ShopProOrderStatusSyncHandler.php": { + "type": "-", + "size": 528, + "lmtime": 1772489139382, + "modified": false + } + }, + "GS1": { + "GS1Service.php": { + "type": "-", + "size": 2412, + "lmtime": 1772132619262, + "modified": false + }, + "MojeGS1Client.php": { + "type": "-", + "size": 6727, + "lmtime": 1771961979753, + "modified": false } }, "Marketplace": { @@ -1929,6 +2381,32 @@ "modified": false } }, + "Orders": { + "OrderImportService.php": { + "type": "-", + "size": 21009, + "lmtime": 1772490222940, + "modified": false + }, + "OrdersController.php": { + "type": "-", + "size": 21408, + "lmtime": 1772496952329, + "modified": false + }, + "OrdersRepository.php": { + "type": "-", + "size": 18855, + "lmtime": 1772496882865, + "modified": false + }, + "OrderStatusSyncService.php": { + "type": "-", + "size": 17295, + "lmtime": 1772489130897, + "modified": false + } + }, "ProductLinks": { "ChannelOffersRepository.php": { "type": "-", @@ -2014,20 +2492,32 @@ }, "IntegrationRepository.php": { "type": "-", - "size": 16971, - "lmtime": 1771883177911, + "size": 25754, + "lmtime": 1772488971508, + "modified": false + }, + "OrderStatusMappingRepository.php": { + "type": "-", + "size": 4135, + "lmtime": 1772489019745, + "modified": false + }, + "OrderStatusRepository.php": { + "type": "-", + "size": 9473, + "lmtime": 1772492415867, "modified": false }, "SettingsController.php": { "type": "-", - "size": 63025, - "lmtime": 1772395757277, + "size": 16872, + "lmtime": 1772493293023, "modified": false }, "ShopProClient.php": { "type": "-", - "size": 29504, - "lmtime": 1772214966055, + "size": 40035, + "lmtime": 1772490209403, "modified": false } }, @@ -2040,29 +2530,24 @@ }, "UsersController.php": { "type": "-", - "size": 5410, - "lmtime": 1772210292906, - "modified": false - } - }, - "GS1": { - "GS1Service.php": { - "type": "-", - "size": 2412, - "lmtime": 1772132619262, - "modified": false - }, - "MojeGS1Client.php": { - "type": "-", - "size": 6727, - "lmtime": 1771961979753, + "size": 5179, + "lmtime": 1772491357326, "modified": false } } } }, "storage": { - "cache": {}, + "cache": { + "phpunit": { + "test-results": { + "type": "-", + "size": 503, + "lmtime": 1772489557946, + "modified": false + } + } + }, "data": { "users.json": { "type": "-", @@ -3089,7 +3574,58 @@ "modified": false } }, - "tmp": {} + "tmp": { + "debug_orders_payload.php": { + "type": "-", + "size": 2431, + "lmtime": 1772489677453, + "modified": false + }, + "phpunit.phar": { + "type": "-", + "size": 5708546, + "lmtime": 1772489549936, + "modified": false + } + } + }, + "TECH_CHANGELOG.md": { + "type": "-", + "size": 373, + "lmtime": 1772490702841, + "modified": false + }, + "tests": { + "bootstrap.php": { + "type": "-", + "size": 759, + "lmtime": 1772489494779, + "modified": false + }, + "Unit": { + "Cron": { + "CronJobTypeTest.php": { + "type": "-", + "size": 603, + "lmtime": 1772489500486, + "modified": false + } + }, + "Settings": { + "OrderStatusMappingRepositoryTest.php": { + "type": "-", + "size": 2415, + "lmtime": 1772489512491, + "modified": false + }, + "ShopProClientTest.php": { + "type": "-", + "size": 972, + "lmtime": 1772489519995, + "modified": false + } + } + } }, "tmp_gs1_test.php": { "type": "-", diff --git a/AGENTS.md b/AGENTS.md index 9e19b66..b722661 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -8,7 +8,13 @@ ## Utrwalanie stalych wymagan - Trwale wymagania techniczne zapisuj w tym pliku (`AGENTS.md`) w root projektu. - Dla zmiennych srodowiskowych utrzymuj tez wpisy w `.env.example`. -- Utrzymuj aktualny opis schematu bazy danych w `DOCS/DB_SCHEMA.md` (aktualizacja przy kazdej zmianie migracji/schematu). +- Dokumentacje techniczna utrzymuj w folderze `DOCS`: + - `DOCS/DB_SCHEMA.md` - aktualny schemat bazy danych (aktualizacja przy kazdej zmianie migracji/schematu), + - `DOCS/ARCHITECTURE.md` - struktura klas, metod, modulow i przeplywow, + - `DOCS/TECH_CHANGELOG.md` - chronologiczny log zmian technicznych (co i dlaczego). +- Przy kazdej nowej funkcji lub zmianie: + - zaktualizuj odpowiednie sekcje w `DOCS/DB_SCHEMA.md`, `DOCS/ARCHITECTURE.md`, `DOCS/TECH_CHANGELOG.md`, + - opisz nowe tabele/kolumny/indeksy/FK, nowe klasy/metody oraz zmiany kontraktow API. ## Alerty i potwierdzenia UI - W aplikacji uzywaj modulu `resources/modules/jquery-alerts` (build do `public/assets/js/modules/jquery-alerts.js` i `public/assets/css/modules/jquery-alerts.css`). @@ -17,3 +23,13 @@ ## Style frontendu - Nie trzymaj styli CSS w plikach widokow (`resources/views/...`). - Wszystkie style umieszczaj w plikach SCSS (`resources/scss/...`) i buduj do `public/assets/css/...`. +- Interfejs ma byc kompaktowy: preferuj mniejsze odstepy i gestszy uklad, tak aby pokazywac jak najwiecej informacji na jednym ekranie bez przewijania. + +## Reuzywalnosc UI +- Nie powielaj kodu takich samych elementow widoku. +- Jezeli ten sam blok UI wystepuje w wiecej niz jednym miejscu, wydziel go do wspolnego komponentu (np. `resources/views/components/...`) i uzywaj ponownie. +- Zmiany wspolnego komponentu musza byc propagowane do wszystkich miejsc uzycia. + +## Srodowisko lokalne (Windows) +- Komenda `php` jest dostepna z instalacji XAMPP (`C:\xampp\php\php.exe`). +- Jezeli `php` nie jest widoczne w terminalu, dodaj `C:\xampp\php` do zmiennej `PATH` (User). diff --git a/DOCS/API.md b/DOCS/API.md deleted file mode 100644 index 1b57aa9..0000000 --- a/DOCS/API.md +++ /dev/null @@ -1,517 +0,0 @@ -# shopPRO REST API - -REST API do integracji z ordersPRO i innymi systemami zewnetrznymi. - -## Autentykacja - -Kazde zapytanie wymaga headera `X-Api-Key` z kluczem API. - -``` -X-Api-Key: {klucz_api} -``` - -Klucz przechowywany jest w `pp_settings` jako parametr `api_key`. API jest stateless (bez sesji). - -## Format odpowiedzi - -### Sukces (HTTP 200) -```json -{ - "status": "ok", - "data": { ... } -} -``` - -### Blad -```json -{ - "status": "error", - "code": "UNAUTHORIZED", - "message": "Invalid or missing API key" -} -``` - -Kody bledow: -| Kod | HTTP | Opis | -|-----|------|------| -| `UNAUTHORIZED` | 401 | Brak lub nieprawidlowy klucz API | -| `BAD_REQUEST` | 400 | Brakujace lub niepoprawne parametry | -| `NOT_FOUND` | 404 | Nie znaleziono zasobu/endpointu/akcji | -| `METHOD_NOT_ALLOWED` | 405 | Nieprawidlowa metoda HTTP | -| `INTERNAL_ERROR` | 500 | Blad wewnetrzny serwera | - -## Endpointy - -### Zamowienia - -#### Lista zamowien -``` -GET api.php?endpoint=orders&action=list -``` - -Parametry filtrowania (opcjonalne): -| Parametr | Typ | Opis | -|----------|-----|------| -| `status` | int | Filtruj po statusie zamowienia | -| `paid` | int (0/1) | Filtruj po statusie platnosci | -| `date_from` | date (YYYY-MM-DD) | Zamowienia od daty | -| `date_to` | date (YYYY-MM-DD) | Zamowienia do daty | -| `updated_since` | datetime (YYYY-MM-DD HH:MM:SS) | Zamowienia zmodyfikowane od podanej daty (klucz do pollingu) | -| `number` | string | Szukaj po numerze zamowienia | -| `client` | string | Szukaj po imieniu, nazwisku lub emailu klienta | -| `page` | int | Numer strony (domyslnie 1) | -| `per_page` | int | Wynikow na strone (domyslnie 50, max 100) | - -Odpowiedz: -```json -{ - "status": "ok", - "data": { - "items": [ - { - "id": 42, - "number": "2026/02/001", - "date_order": "2026-02-19 10:30:00", - "updated_at": "2026-02-19 12:00:00", - "status": 4, - "paid": 1, - "client_name": "Jan", - "client_surname": "Kowalski", - "client_email": "jan@example.com", - "client_phone": "111222333", - "client_street": "Testowa 1", - "client_postal_code": "00-000", - "client_city": "Warszawa", - "firm_name": null, - "firm_nip": null, - "transport": "Kurier DPD", - "transport_cost": 15.00, - "payment_method": "Przelew bankowy", - "summary": 150.00 - } - ], - "total": 1, - "page": 1, - "per_page": 50 - } -} -``` - -#### Szczegoly zamowienia -``` -GET api.php?endpoint=orders&action=get&id={order_id} -``` - -Zwraca pelne dane zamowienia z produktami i historia statusow. - -#### Zmiana statusu zamowienia -``` -PUT api.php?endpoint=orders&action=change_status&id={order_id} -Content-Type: application/json - -{ - "status_id": 5, - "send_email": true -} -``` - -Odpowiedz: -```json -{ - "status": "ok", - "data": { - "order_id": 42, - "status_id": 5, - "changed": true - } -} -``` - -#### Oznacz jako oplacone -``` -PUT api.php?endpoint=orders&action=set_paid&id={order_id} -``` - -Opcjonalnie w body: `{"send_email": true}` - -#### Oznacz jako nieoplacone -``` -PUT api.php?endpoint=orders&action=set_unpaid&id={order_id} -``` - -### Produkty - -#### Lista produktow -``` -GET api.php?endpoint=products&action=list -``` - -Parametry filtrowania (opcjonalne): -| Parametr | Typ | Opis | -|----------|-----|------| -| `search` | string | Szukaj po nazwie, EAN lub SKU | -| `status` | int (0/1) | Filtruj po statusie (1 = aktywny, 0 = nieaktywny) | -| `promoted` | int (0/1) | Filtruj po promocji | -| `attribute_{id}` | int | Filtruj po atrybucie — `attribute_1=3` oznacza atrybut 1 = wartosc 3 (wiele filtrow AND) | -| `sort` | string | Sortuj po: id, name, price_brutto, status, promoted, quantity (domyslnie id) | -| `sort_dir` | string | Kierunek: ASC lub DESC (domyslnie DESC) | -| `page` | int | Numer strony (domyslnie 1) | -| `per_page` | int | Wynikow na strone (domyslnie 50, max 100) | - -Odpowiedz: -```json -{ - "status": "ok", - "data": { - "items": [ - { - "id": 1, - "sku": "PROD-001", - "ean": "5901234123457", - "name": "Produkt testowy", - "price_brutto": 99.99, - "price_brutto_promo": null, - "price_netto": 81.29, - "price_netto_promo": null, - "quantity": 10, - "status": 1, - "promoted": 0, - "vat": 23, - "weight": 0.5, - "main_image": "product1.jpg", - "date_add": "2026-01-15 10:00:00", - "date_modify": "2026-02-19 12:00:00" - } - ], - "total": 1, - "page": 1, - "per_page": 50 - } -} -``` - -#### Szczegoly produktu -``` -GET api.php?endpoint=products&action=get&id={product_id} -``` - -Zwraca pelne dane produktu z jezykami, zdjeciami, kategoriami i atrybutami. - -Odpowiedz: -```json -{ - "status": "ok", - "data": { - "id": 1, - "sku": "PROD-001", - "ean": "5901234123457", - "price_brutto": 99.99, - "price_brutto_promo": null, - "price_netto": 81.29, - "price_netto_promo": null, - "quantity": 10, - "status": 1, - "promoted": 0, - "vat": 23, - "weight": 0.5, - "stock_0_buy": 0, - "custom_label_0": null, - "set_id": null, - "product_unit_id": 1, - "producer_id": 3, - "date_add": "2026-01-15 10:00:00", - "date_modify": "2026-02-19 12:00:00", - "languages": { - "pl": { - "name": "Produkt testowy", - "short_description": "Krotki opis", - "description": "

Pelny opis produktu

", - "meta_description": null, - "meta_keywords": null, - "meta_title": null, - "seo_link": "produkt-testowy", - "copy_from": null, - "warehouse_message_zero": null, - "warehouse_message_nonzero": null, - "tab_name_1": null, - "tab_description_1": null, - "tab_name_2": null, - "tab_description_2": null, - "canonical": null - } - }, - "images": [ - {"id": 1, "src": "product1.jpg", "alt": "Zdjecie produktu"} - ], - "categories": [1, 5], - "attributes": [ - { - "attribute_id": 1, - "attribute_type": 1, - "attribute_names": {"pl": "Kolor", "en": "Color"}, - "value_id": 3, - "value_names": {"pl": "Czerwony", "en": "Red"} - } - ], - "variants": [ - { - "id": 101, - "permutation_hash": "1-3|2-5", - "sku": "PROD-001-RED-L", - "ean": null, - "price_brutto": 109.99, - "price_brutto_promo": null, - "price_netto": 89.42, - "price_netto_promo": null, - "quantity": 5, - "stock_0_buy": 0, - "weight": 0.5, - "status": 1, - "attributes": [ - {"attribute_id": 1, "attribute_names": {"pl": "Kolor"}, "value_id": 3, "value_names": {"pl": "Czerwony"}}, - {"attribute_id": 2, "attribute_names": {"pl": "Rozmiar"}, "value_id": 5, "value_names": {"pl": "L"}} - ] - } - ] - } -} -``` - -#### Tworzenie produktu -``` -POST api.php?endpoint=products&action=create -Content-Type: application/json - -{ - "price_brutto": 99.99, - "vat": 23, - "quantity": 10, - "status": 1, - "sku": "PROD-001", - "ean": "5901234123457", - "weight": 0.5, - "languages": { - "pl": { - "name": "Nowy produkt", - "description": "

Opis produktu

" - } - }, - "categories": [1, 5], - "products_related": [10, 20] -} -``` - -Wymagane: `languages` (min. 1 jezyk z `name`) oraz `price_brutto`. - -Odpowiedz (HTTP 201): -```json -{ - "status": "ok", - "data": { - "id": 42 - } -} -``` - -#### Aktualizacja produktu -``` -PUT api.php?endpoint=products&action=update&id={product_id} -Content-Type: application/json - -{ - "price_brutto": 129.99, - "status": 1, - "languages": { - "pl": { - "name": "Zaktualizowana nazwa" - } - } -} -``` - -Partial update — wystarczy przeslac tylko zmienione pola. Pola nieprzeslane zachowuja aktualna wartosc. - -Odpowiedz: pelne dane produktu (jak w `get`). - -### Warianty produktow - -#### Lista wariantow produktu -``` -GET api.php?endpoint=products&action=variants&id={product_id} -``` - -Zwraca warianty produktu nadrzednego wraz z dostepnymi atrybutami. - -Odpowiedz: -```json -{ - "status": "ok", - "data": { - "product_id": 1, - "available_attributes": [ - { - "id": 1, - "type": 1, - "status": 1, - "names": {"pl": "Kolor", "en": "Color"}, - "values": [ - {"id": 3, "names": {"pl": "Czerwony", "en": "Red"}, "is_default": 0, "impact_on_the_price": null}, - {"id": 4, "names": {"pl": "Niebieski", "en": "Blue"}, "is_default": 0, "impact_on_the_price": 10.0} - ] - } - ], - "variants": [ - { - "id": 101, - "permutation_hash": "1-3", - "sku": "PROD-001-RED", - "ean": null, - "price_brutto": 109.99, - "price_brutto_promo": null, - "price_netto": 89.42, - "price_netto_promo": null, - "quantity": 5, - "stock_0_buy": 0, - "weight": 0.5, - "status": 1, - "attributes": [ - {"attribute_id": 1, "attribute_names": {"pl": "Kolor"}, "value_id": 3, "value_names": {"pl": "Czerwony"}} - ] - } - ] - } -} -``` - -#### Tworzenie wariantu -``` -POST api.php?endpoint=products&action=create_variant&id={product_id} -Content-Type: application/json - -{ - "attributes": {"1": 3, "2": 5}, - "sku": "PROD-001-RED-L", - "ean": "5901234123458", - "price_brutto": 109.99, - "quantity": 5, - "weight": 0.5 -} -``` - -Wymagane: `attributes` (mapa attribute_id -> value_id, min. 1). Kombinacja atrybutow musi byc unikalna. - -Odpowiedz (HTTP 201): pelne dane wariantu. - -#### Aktualizacja wariantu -``` -PUT api.php?endpoint=products&action=update_variant&id={variant_id} -Content-Type: application/json - -{ - "sku": "PROD-001-RED-XL", - "price_brutto": 119.99, - "quantity": 3 -} -``` - -Partial update — mozna zmienic: sku, ean, price_brutto, price_netto, price_brutto_promo, price_netto_promo, quantity, stock_0_buy, weight, status. - -Odpowiedz: pelne dane wariantu. - -#### Usuwanie wariantu -``` -DELETE api.php?endpoint=products&action=delete_variant&id={variant_id} -``` - -Odpowiedz: -```json -{ - "status": "ok", - "data": {"id": 101, "deleted": true} -} -``` - -### Slowniki - -#### Lista statusow zamowien -``` -GET api.php?endpoint=dictionaries&action=statuses -``` - -Odpowiedz: -```json -{ - "status": "ok", - "data": [ - {"id": 0, "name": "Nowe"}, - {"id": 1, "name": "Oplacone"}, - {"id": 4, "name": "W realizacji"}, - {"id": 6, "name": "Wyslane"} - ] -} -``` - -#### Lista metod transportu -``` -GET api.php?endpoint=dictionaries&action=transports -``` - -#### Lista metod platnosci -``` -GET api.php?endpoint=dictionaries&action=payment_methods -``` - -#### Lista atrybutow -``` -GET api.php?endpoint=dictionaries&action=attributes -``` - -Zwraca aktywne atrybuty z wartosciami i wielojezycznymi nazwami. - -Odpowiedz: -```json -{ - "status": "ok", - "data": [ - { - "id": 1, - "type": 1, - "status": 1, - "names": {"pl": "Kolor", "en": "Color"}, - "values": [ - {"id": 3, "names": {"pl": "Czerwony"}, "is_default": 0, "impact_on_the_price": null}, - {"id": 4, "names": {"pl": "Niebieski"}, "is_default": 1, "impact_on_the_price": 10.0} - ] - } - ] -} -``` - -## Polling - -Aby pobierac tylko nowe/zmienione zamowienia, uzyj parametru `updated_since`: - -``` -GET api.php?endpoint=orders&action=list&updated_since=2026-02-19 12:00:00 -``` - -Kolumna `updated_at` w `pp_shop_orders` jest aktualizowana automatycznie przy kazdej modyfikacji zamowienia (zmiana statusu, platnosci, edycja danych, tworzenie zamowienia). - -## Konfiguracja - -Klucz API ustawia sie w panelu admina w ustawieniach sklepu lub bezposrednio w bazie: - -```sql -INSERT INTO pp_settings (param, value) VALUES ('api_key', 'twoj-klucz-api'); --- lub -UPDATE pp_settings SET value = 'twoj-klucz-api' WHERE param = 'api_key'; -``` - -## Architektura - -- Entry point: `api.php` -- Router: `\api\ApiRouter` (`autoload/api/ApiRouter.php`) -- Kontrolery: `autoload/api/Controllers/` - - `OrdersApiController` — zamowienia (5 akcji) - - `ProductsApiController` — produkty (8 akcji: list, get, create, update, variants, create_variant, update_variant, delete_variant) - - `DictionariesApiController` — slowniki (4 akcje: statuses, transports, payment_methods, attributes) diff --git a/DOCS/ARCHITECTURE.md b/DOCS/ARCHITECTURE.md new file mode 100644 index 0000000..bb3cc60 --- /dev/null +++ b/DOCS/ARCHITECTURE.md @@ -0,0 +1,152 @@ +# Architecture + +## Status +- Projekt po resecie do trybu `users-only`. + +## Moduly aktywne +- `App\Modules\Auth` +- `App\Modules\Orders` +- `App\Modules\Users` +- `App\Modules\Settings` + +## Routing +- `GET /login`, `POST /login`, `POST /logout` +- `GET /settings/users`, `POST /settings/users` +- `GET /orders` (redirect do `/orders/list`) +- `GET /orders/list` +- `GET /orders/{id}` +- `GET /users` (redirect do `/settings/users`) +- `POST /users` (compat route) +- `GET /settings` (redirect do `/settings/users`) +- `GET /settings/database` +- `POST /settings/database/migrate` +- `GET /settings/statuses` +- `POST /settings/status-groups` +- `POST /settings/status-groups/update` +- `POST /settings/status-groups/delete` +- `POST /settings/status-groups/reorder` +- `POST /settings/statuses/create` +- `POST /settings/statuses/update` +- `POST /settings/statuses/delete` +- `POST /settings/statuses/reorder` +- `GET /health`, `GET /` (redirect) + +## Korekta logowania +- `AuthController::showLogin(Request): Response`: + - dla zalogowanego usera redirect na `/settings/users` (zamiast nieistniejacego `/dashboard`). +- `AuthController::login(Request): Response`: + - po poprawnym logowaniu redirect na `/settings/users`. + +## Kluczowe klasy +- `App\Core\Application` +- `App\Modules\Auth\AuthController` +- `App\Modules\Auth\AuthService` +- `App\Modules\Orders\OrdersController` +- `App\Modules\Orders\OrdersRepository` +- `App\Modules\Settings\SettingsController` +- `App\Modules\Settings\OrderStatusRepository` +- `App\Modules\Users\UsersController` +- `App\Modules\Users\UserRepository` + +## Przeplyw Zamowienia > Lista zamowien +- `GET /orders/list`: + - `OrdersController::index(Request): Response` + - pobiera dane listy przez `OrdersRepository::paginate(...)`, + - pobiera slowniki filtrow (`sourceOptions()`, `statusOptions()`), statystyki (`quickStats()`), agregaty statusow (`statusCounts()`) i konfiguracje grup/statusow (`statusPanelConfig()`), + - buduje panel statusow z grupami i licznikami (`buildStatusPanel(...)`) z linkami filtrujacymi po statusie, + - panel statusow i etykiety statusow sa zgodne z konfiguracja z `Ustawienia > Statusy` (z fallbackiem `Pozostale`), + - renderuje podglad pozycji zamowienia (nazwa, miniatura, ilosc) na bazie `order_items`, + - obsluguje modal podgladu zdjecia pozycji po kliknieciu miniatury, + - normalizuje status techniczny na etykiete biznesowa (bez kodu statusu), + - renderuje widok `resources/views/orders/list.php` i komponent tabeli `resources/views/components/table-list.php`. +- `GET /orders/{id}`: + - `OrdersController::show(Request): Response` + - pobiera szczegoly przez `OrdersRepository::findDetails(int $orderId)`, statystyke statusow przez `statusCounts()` oraz konfiguracje przez `statusPanelConfig()`, + - buduje panel statusow z grupami i licznikami (`buildStatusPanel(...)`), + - renderuje klikalne taby sekcji i przelaczanie paneli po stronie klienta (JS w `orders/show.php`), + - renderuje widok `resources/views/orders/show.php` z sekcjami: + - pozycje zamowienia, + - szczegoly zamowienia, + - platnosc i wysylka, + - adresy (`customer`, `invoice`, `delivery`), + - notatki i historia statusow. +- Sidebar ma oddzielna grupe nawigacyjna: + - `Zamowienia` -> `Lista zamowien`. + +## Skrypty techniczne (CLI) +- `bin/fix_status_codes.php` + - naprawa kodow grup/statusow (transliteracja PL -> ASCII, tryb `--dry-run`, opcja `--use-remote`). +- `bin/deploy_and_seed_orders.php` + - aplikuje generyczny schema zamowien z `database/drafts/20260302_orders_schema_v1.sql`, + - seeduje dane testowe (`--count`, `--append`, `--use-remote`, `--profile=default|realistic`), + - profil `realistic` utrzymuje spojne zaleznosci miedzy: + - statusem zamowienia, + - statusem i kwota platnosci, + - obecnoscia wysylek i dokumentow, + - historia przejsc statusow (deterministyczne sciezki zamiast losowych przeskokow). + +## Przeplyw Ustawienia > Statusy +- `GET /settings/statuses`: + - `SettingsController::statuses(Request): Response` + - pobiera dane przez `OrderStatusRepository::listGroups()` i `OrderStatusRepository::listStatuses()`, + - renderuje widok `resources/views/settings/statuses.php`. +- `POST /settings/status-groups`: + - `SettingsController::createStatusGroup(Request): Response` + - waliduje CSRF i dane (`name`, `color_hex`, `is_active`), + - `code` jest generowany automatycznie z `name` i nie jest edytowany z UI, + - zapisuje przez `OrderStatusRepository::createGroup(...)`. +- `POST /settings/status-groups/update`: + - `SettingsController::updateStatusGroup(Request): Response` + - waliduje istnienie grupy i aktualizuje rekord przez `updateGroup(...)`, + - `code` pozostaje bez zmian (read-only po utworzeniu). +- `POST /settings/status-groups/delete`: + - `SettingsController::deleteStatusGroup(Request): Response` + - usuwa grupe przez `deleteGroup(...)`; statusy z tej grupy usuwane sa kaskadowo (FK). +- `POST /settings/status-groups/reorder`: + - `SettingsController::reorderStatusGroups(Request): Response` + - zapisuje kolejnosc drag-and-drop grup przez `OrderStatusRepository::reorderGroups(...)`, + - endpoint jest wywolywany automatycznie po upuszczeniu elementu listy (auto-save). +- `POST /settings/statuses/create`: + - `SettingsController::createStatus(Request): Response` + - waliduje grupe, pola statusu i zapisuje przez `createStatus(...)`. +- `POST /settings/statuses/update`: + - `SettingsController::updateStatus(Request): Response` + - waliduje dane i aktualizuje status przez `updateStatus(...)`, + - `code` pozostaje bez zmian (read-only po utworzeniu). +- `POST /settings/statuses/delete`: + - `SettingsController::deleteStatus(Request): Response` + - usuwa status przez `deleteStatus(...)`. +- `POST /settings/statuses/reorder`: + - `SettingsController::reorderStatuses(Request): Response` + - zapisuje kolejnosc drag-and-drop statusow w ramach grupy przez `OrderStatusRepository::reorderStatusesByGroup(...)`, + - endpoint jest wywolywany automatycznie po upuszczeniu elementu listy (auto-save). + +## Nawigacja ustawien +- Sidebar (`resources/views/layouts/app.php`) ma nowy podlink: + - `Statusy` (`/settings/statuses`). + +## Przeplyw Ustawienia > Baza danych +- `GET /settings/database`: + - `SettingsController::database(Request): Response` + - pobiera `Migrator::status()`, przekazuje statystyki i liste pending migracji do widoku `resources/views/settings/database.php`. +- `POST /settings/database/migrate`: + - `SettingsController::migrate(Request): Response` + - waliduje CSRF, + - uruchamia `Migrator::runPending()`, + - zapisuje wynik do flash (`settings_success` / `settings_error`, `settings_migrate_logs`), + - wykonuje redirect do `GET /settings/database`. + +## Zmiany nawigacji +- Sidebar ma teraz grupe `Ustawienia` z podlinkami: + - `Uzytkownicy` (`/settings/users`) + - `Baza danych` (`/settings/database`) +- `UsersController::index(Request): Response` ustawia: + - `activeMenu = settings` + - `activeSettings = users` +- Usunieto wewnetrzny pasek `settings-nav` z widokow podstron ustawien. + +## Zasady aktualizacji +- Przy kazdej zmianie dopisz: + - nowe klasy i metody (sygnatury + odpowiedzialnosc), + - zmiany przeplywu request -> controller -> repository, + - kontrakty wejscia/wyjscia istotnych metod. diff --git a/DOCS/BACKLOG_MIKROZADANIA.md b/DOCS/BACKLOG_MIKROZADANIA.md deleted file mode 100644 index 69dfe7a..0000000 --- a/DOCS/BACKLOG_MIKROZADANIA.md +++ /dev/null @@ -1,113 +0,0 @@ -# orderPRO - Backlog mikro-zadania (MVP) - -Data: 2026-02-19 - -Legenda statusow: -- `TODO` - do zrobienia -- `DOING` - w trakcie -- `DONE` - zakonczone -- `BLOCKED` - zablokowane - -## Sprint 0 - Fundament - -1. `DONE` Utworzyc strukture katalogow projektu (`public`, `src`, `config`, `database`, `storage`, `bin`). -2. `DONE` Dodac `composer.json` z podstawowymi bibliotekami. -3. `DONE` Przygotowac `public/index.php` jako front controller. -4. `DONE` Dodac prosty router i testowa trase `/health`. -5. `DONE` Dodac loader konfiguracji `.env`. -6. `TODO` Podlaczyc MySQL (polaczenie + test zapytania). -7. `TODO` Utworzyc migracje tabeli `users`. -8. `TODO` Dodac seed pierwszego uzytkownika admin. -9. `TODO` Przygotowac bazowy layout HTML panelu (`header`, `sidebar`, `content`). -10. `DONE` Przygotowac wyglad strony logowania (sam widok). -11. `DONE` Dodac formularz logowania (email + haslo + submit). -12. `DONE` Dodac endpoint POST logowania i walidacje danych. -13. `DONE` Dodac sesje uzytkownika po poprawnym logowaniu. -14. `DONE` Dodac middleware sprawdzajace zalogowanie. -15. `DONE` Dodac wylogowanie (`POST /logout`). -16. `DONE` Dodac komunikaty bledow logowania w widoku. -17. `DONE` Dodac CSRF token do formularzy. -18. `DONE` Dodac podstawowe logowanie bledow do pliku. -19. `DONE` Przeniesc teksty UI do systemu tlumaczen (`resources/lang/pl.php` + helper `$t()`). -20. `DONE` Dodac konfiguracje locale i podlaczyc translator do widokow i kontrolerow. -21. `DONE` Przeniesc style do SCSS i uruchomic kompilacje/minifikacje do `public/assets/css`. -22. `DONE` Dodac skrypty frontendowe do budowy styli (`npm run build:css`, `npm run watch:css`). -23. `DONE` Ujednolicic design panelu i logowania pod styl adsPRO. - -## Sprint 1 - Zamowienia z wlasnego sklepu - -24. `TODO` Utworzyc migracje `orders`, `order_items`, `order_addresses`. -25. `TODO` Dodac encje/repozytoria dla zamowien. -26. `TODO` Dodac ekran listy zamowien (`/orders`) z paginacja. -27. `TODO` Dodac ekran szczegolow zamowienia (`/orders/{id}`). -28. `TODO` Dodac zmiane statusu zamowienia przyciskiem. -29. `TODO` Dodac tabele `order_status_history`. -30. `TODO` Zapisywac historie kazdej zmiany statusu. -31. `TODO` Dodac konfiguracje kanalu "wlasny sklep" w tabeli `channels`. -32. `TODO` Dodac klienta API wlasnego sklepu. -33. `TODO` Dodac importer zamowien (reczne uruchomienie z panelu). -34. `TODO` Dodac deduplikacje zamowien po `external_order_id`. -35. `TODO` Dodac cron do cyklicznego importu. - -## Sprint 2 - Allegro - -36. `TODO` Utworzyc konfiguracje Allegro w `channels`. -37. `TODO` Dodac ekran podpiecia konta Allegro (OAuth start/callback). -38. `TODO` Zapisac i odswiezac tokeny Allegro. -39. `TODO` Dodac importer zamowien Allegro. -40. `TODO` Dodac mapowanie statusow Allegro -> statusy lokalne. -41. `TODO` Dodac logowanie bledow API Allegro do `sync_errors`. - -## Sprint 3 - Apaczka - -42. `TODO` Utworzyc migracje `shipments`, `shipment_labels`. -43. `TODO` Dodac konfiguracje API Apaczka. -44. `TODO` Dodac przycisk "Utworz przesylke" na szczegolach zamowienia. -45. `TODO` Dodac wysylke danych paczki do Apaczka. -46. `TODO` Zapisac numer tracking i status utworzenia. -47. `TODO` Dodac pobieranie etykiety PDF. -48. `TODO` Dodac cron odswiezajacy statusy przesylek. - -## Sprint 4 - Dokumenty i magazyn uproszczony - -49. `TODO` Utworzyc migracje `documents`, `inventory_items`, `inventory_reservations`. -50. `TODO` Dodac integracje z systemem dokumentow (API zewnetrzne). -51. `TODO` Dodac przycisk "Wystaw dokument" na szczegolach zamowienia. -52. `TODO` Zapisac identyfikator/link dokumentu z systemu zewnetrznego. -53. `TODO` Dodac rezerwacje stanu po imporcie zamowienia. -54. `TODO` Dodac zwolnienie rezerwacji po anulowaniu zamowienia. -55. `TODO` Dodac prosty widok stanow magazynowych. - -## Sprint 5 - Stabilizacja - -56. `TODO` Dodac tabele `jobs` i `failed_jobs`. -57. `TODO` Przeniesc ciezsze operacje integracyjne do jobow. -58. `TODO` Dodac retry z rosnacym opoznieniem. -59. `TODO` Dodac panel "Bledy synchronizacji". -60. `TODO` Dodac testy najwazniejszych flow (logowanie, import, przesylka). -61. `TODO` Dodac skrypt backupu bazy (cron nocny). - -## Nastepne zadanie (start) -1. `TODO` Podlaczyc MySQL (polaczenie + test zapytania): -- klasa polaczenia PDO, -- konfiguracja z `.env`, -- prosty endpoint kontrolny DB. - -## Sprint 1.5 - Modul produktow (bez importu/eksportu) - -62. `DONE` Utworzyc migracje tabel: `products`, `product_translations`, `product_images`, `product_categories`. -63. `DONE` Utworzyc migracje tabel: `product_variants`, `product_variant_attributes`. -64. `DONE` Utworzyc migracje tabel slownikowych: `attributes`, `attribute_translations`, `attribute_values`, `attribute_value_translations`. -65. `DONE` Utworzyc migracje tabel pomocniczych: `product_change_log`, `sales_channels`, `product_channel_map`. -66. `DONE` Dodac repozytoria i serwis domenowy dla produktow. -67. `DONE` Dodac walidator produktow i wariantow (nazwa, cena, SKU produktu i wariantu, EAN, kombinacje atrybutow). -68. `DONE` Dodac ekran listy produktow (`/products`) z filtrami i paginacja. -69. `DONE` Dodac ekran tworzenia produktu (`/products/create`) i zapis (`POST /products`). -70. `DONE` Dodac ekran szczegolow i edycji produktu (`/products/edit?id={id}`). -71. `TODO` Dodac obsluge wariantow (dodaj/edytuj) na szczegolach produktu + konwersja simple <-> variant_parent. -72. `TODO` Dodac sekcje atrybutow i wartosci (lista + dodawanie). -73. `DONE` Dodac wpisy do `product_change_log` przy zmianach krytycznych. -74. `DONE` Dodac pozycje "Produkty" do nawigacji + tlumaczenia `resources/lang/pl.php`. -75. `DONE` Dodac dwukierunkowe przeliczanie cen brutto/netto (UI + backend) na podstawie VAT. -76. `TODO` Dodac upload i zapis zdjec produktu na serwerze orderPRO. -77. `TODO` Przeprowadzic test manualny flow: create/edit/list/filter/variant. diff --git a/DOCS/CRON_QUEUE.md b/DOCS/CRON_QUEUE.md deleted file mode 100644 index 0d5d794..0000000 --- a/DOCS/CRON_QUEUE.md +++ /dev/null @@ -1,27 +0,0 @@ -# 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/DB_SCHEMA.md b/DOCS/DB_SCHEMA.md index d76bf3d..4d3aa11 100644 --- a/DOCS/DB_SCHEMA.md +++ b/DOCS/DB_SCHEMA.md @@ -1,119 +1,79 @@ -# Struktura bazy danych (orderPRO) +# DB Schema -## Cel pliku -- Ten dokument opisuje aktualny schemat bazy danych na podstawie migracji w `database/migrations`. -- Aktualizuj ten plik przy każdej zmianie schematu (nowa tabela, kolumna, indeks, klucz obcy). +## Status +- Projekt po resecie do trybu `users-only`. +- Aktualizuj ten plik przy kazdej zmianie migracji/schematu. +- 2026-03-02: Przywrocenie UI `Ustawienia > Baza danych` nie wprowadza zmian w schemacie. +- 2026-03-02: Dodano tabele statusow (grupy + statusy) dla nowej zakladki `Ustawienia > Statusy`. +- 2026-03-02: Przygotowano draft generycznego schematu zamowien (bez aktywnej migracji) w `database/drafts/20260302_orders_schema_v1.sql`. +- 2026-03-03: Wdrozono generyczne tabele zamowien na bazie docelowej skryptem `bin/deploy_and_seed_orders.php` (bez migratora SQL). +- 2026-03-03: Dodano UI `Zamowienia > Lista zamowien` - bez zmian schematu (wykorzystuje istniejace tabele domeny zamowien). +- 2026-03-03: Dodano UI `Zamowienia > Szczegoly zamowienia` (`GET /orders/{id}`) - bez zmian schematu. -## Tabele i przeznaczenie +## Tabele ### `users` - Uzytkownicy panelu. - Klucz unikalny: `email`. -### `products` -- Glowna tabela produktow lokalnych. -- Najwazniejsze pola: `type`, `sku`, `ean`, `status`, `promoted`, `vat`, `weight`, `price_*`, `quantity`. -- Pola shopPRO: `new_to_date`, `additional_message`, `additional_message_required`, `additional_message_text`. -- Dodatkowe: `producer_id`, `producer_name`, `product_unit_id`, `custom_fields_json`. -- Soft delete: `deleted_at`. +### `order_status_groups` +- Grupy statusow zamowien zarzadzane z UI. +- Kolumny: + - `id` (PK, int unsigned, AI), + - `name` (varchar 120), + - `code` (varchar 64, UNIQUE), + - `color_hex` (char 7, domyslnie `#64748b`), + - `sort_order` (int, domyslnie `0`), + - `is_active` (tinyint(1), domyslnie `1`), + - `created_at`, `updated_at`. +- Indeksy: + - `order_status_groups_code_unique` (UNIQUE: `code`), + - `order_status_groups_sort_order_idx` (`sort_order`). -### `product_translations` -- Globalne tresci produktu per jezyk (`lang`). -- Najwazniejsze pola: `name`, `short_description`, `description`, SEO, `security_information`. -- Unikalnosc: `(product_id, lang)`. +### `order_statuses` +- Statusy przypisane do grup statusow. +- Kolumny: + - `id` (PK, int unsigned, AI), + - `group_id` (FK -> `order_status_groups.id`), + - `name` (varchar 120), + - `code` (varchar 64, UNIQUE), + - `sort_order` (int, domyslnie `0`), + - `is_active` (tinyint(1), domyslnie `1`), + - `created_at`, `updated_at`. +- Indeksy: + - `order_statuses_code_unique` (UNIQUE: `code`), + - `order_statuses_group_sort_idx` (`group_id`, `sort_order`, `id`). +- Klucze obce: + - `order_statuses_group_fk`: `group_id` -> `order_status_groups.id` (`ON DELETE CASCADE`, `ON UPDATE CASCADE`). -### `product_integration_translations` -- Nadpisania tresci produktu per integracja shopPRO. -- Pola: `name`, `short_description`, `description` (NULL = fallback do `product_translations`). -- Unikalnosc: `(product_id, integration_id)`. +### Domena zamowien (generyczna) +- Wdrozone tabele: + - `orders` + - `order_addresses` + - `order_items` + - `order_payments` + - `order_shipments` + - `order_documents` + - `order_notes` + - `order_status_history` + - `order_tags_dict` + - `order_tag_links` + - `integration_order_sync_state` +- Charakterystyka: + - schema neutralna wzgledem dostawcy API (pola `source_*`, `external_*`), + - kolekcje zamowienia rozdzielone na osobne tabele 1:N, + - `payload_json` dostepne dla diagnostyki/replay, + - historia zmian statusow utrzymywana w `order_status_history`. -### `product_images` -- Obrazy produktu (`storage_path`, `is_main`, `sort_order`). +## Zasady aktualizacji +- Po kazdej migracji dopisz: + - nowe/zmienione tabele i kolumny, + - indeksy i klucze obce, + - wplyw na dane i kompatybilnosc wsteczna. -### `product_categories` -- Relacja M:N produkt-kategoria (lokalna). - -### `attributes` -- Definicje atrybutow wariantow. - -### `attribute_translations` -- Tlumaczenia nazw atrybutow per jezyk. - -### `attribute_values` -- Wartosci atrybutow (np. kolor, rozmiar), z opcjonalnym `impact_on_price`. - -### `attribute_value_translations` -- Tlumaczenia wartosci atrybutow per jezyk. - -### `product_variants` -- Warianty produktu. -- Najwazniejsze pola: `permutation_hash`, `sku`, `ean`, `status`, `price_*`, `weight`, `stock_0_buy`. -- Unikalnosc: `sku`, `(product_id, permutation_hash)`. - -### `product_variant_attributes` -- Relacja wariant -> (atrybut, wartosc). -- Klucz glowny: `(variant_id, attribute_id)`. - -### `product_change_log` -- Log zmian produktow (audyt JSON: `before_json`, `after_json`). - -### `sales_channels` -- Slownik kanalow sprzedazy (`shoppro`, `allegro`, `erli`, ...). - -### `product_channel_map` -- Mapowanie lokalnego produktu do kanalu/integracji i ID zewnetrznych. -- Najwazniejsze pola: `integration_id`, `external_product_id`, `external_variant_id`, `sync_state`, `link_type`, `link_status`, `confidence`. -- Pola audytowe powiazania: `linked_at`, `linked_by_user_id`, `unlinked_at`, `unlinked_by_user_id`, `sync_meta_json`. - -### `channel_offers` -- Cache ofert zewnetrznych dla integracji. -- Najwazniejsze pola: `external_product_id`, `external_variant_id`, `external_offer_id`, `name` (tytul oferty), `sku`, `ean`, `offer_status`, `last_seen_at`, `payload_json`. -- Unikalnosc: `(integration_id, external_product_id, external_variant_id)`. - -### `product_link_events` -- Historia zdarzen na powiazaniach (`product_channel_map`). - -### `integrations` -- Konfiguracja instancji integracji (obecnie shopPRO). -- Najwazniejsze pola: `type`, `name`, `base_url`, `api_key_encrypted`, `timeout_seconds`, `is_active`. - -### `integration_test_logs` -- Historia testow polaczen integracji. - -### `cron_jobs` -- Kolejka jobow crona. - -### `cron_schedules` -- Harmonogramy okresowych jobow. -- Aktualnie zawiera m.in.: - - `product_links_health_check` - - `shoppro_offer_titles_refresh` (odswiezanie tytulow ofert; domyslnie co 30 dni) - -### `product_link_alerts` -- Alerty zdrowia powiazan produktu. - -### `app_settings` -- Ustawienia aplikacyjne key-value. -- Przykładowe klucze: `cron_run_on_web`, `cron_web_limit`, `gs1_*`, `products_sku_format`. - -## Relacje (skrot) -- `product_translations.product_id -> products.id` -- `product_integration_translations.product_id -> products.id` -- `product_integration_translations.integration_id -> integrations.id` -- `product_images.product_id -> products.id` -- `product_variants.product_id -> products.id` -- `product_variant_attributes.variant_id -> product_variants.id` -- `product_variant_attributes.attribute_id -> attributes.id` -- `product_variant_attributes.value_id -> attribute_values.id` -- `product_channel_map.product_id -> products.id` -- `product_channel_map.channel_id -> sales_channels.id` -- `product_channel_map.integration_id -> integrations.id` -- `channel_offers.integration_id -> integrations.id` -- `channel_offers.channel_id -> sales_channels.id` -- `product_link_events.product_channel_map_id -> product_channel_map.id` -- `product_link_alerts.product_channel_map_id -> product_channel_map.id` - -## Jak utrzymywac dokument -1. Po dodaniu migracji zaktualizuj sekcje tabel/kolumn/relacji. -2. Gdy dodajesz nowe klucze do `app_settings`, dopisz je tu. -3. Przy zmianach harmonogramu crona zaktualizuj liste jobow w `cron_schedules`. +## Drafty (nieaktywne) +- `database/drafts/20260302_orders_schema_v1.sql`: + - propozycja normalizacji domeny zamowien pod integracje zewnetrzne (`orders`, `order_items`, `order_status_history`, platnosci, wysylki, dokumenty, notatki, tagi, sync-state), + - plik nie jest odpalany przez obecny migrator (`database/migrations/*.sql`). +- `bin/deploy_and_seed_orders.php`: + - techniczny skrypt wdrozeniowy, ktory aplikuje schema z draftu i opcjonalnie seeduje dane testowe. diff --git a/DOCS/FRONTEND_STANDARDS.md b/DOCS/FRONTEND_STANDARDS.md deleted file mode 100644 index 297de47..0000000 --- a/DOCS/FRONTEND_STANDARDS.md +++ /dev/null @@ -1,17 +0,0 @@ -# Frontend Standards - -## 1) Wspolne style UI -- Powtarzalne elementy (np. przyciski, tabele, paginacja, alerty, pola formularzy) trzymamy w: - - `resources/scss/shared/_ui-components.scss` -- Widoki maja korzystac z klas wspolnych (`btn`, `table`, `pagination`, `form-control`, `alert`) zamiast duplikowania stylu lokalnie. - -## 2) Moduly JS wielokrotnego uzycia -- Kazdy modul przenoszalny trzymamy w oddzielnym folderze: - - `resources/modules//` -- Minimalny zestaw plikow: - - `resources/modules//.js` - - `resources/modules//.scss` -- Modul ma byc niezalezny od logiki projektu (brak hardcoded sciezek i zaleznosci biznesowych). - -## 3) Przyklad -- Referencyjny modul: `resources/modules/jquery-alerts/` diff --git a/DOCS/MIGRATIONS.md b/DOCS/MIGRATIONS.md deleted file mode 100644 index 8c32225..0000000 --- a/DOCS/MIGRATIONS.md +++ /dev/null @@ -1,19 +0,0 @@ -# Migracje bazy danych - -## Zasada -- Kazda zmiana schematu bazy to nowy plik `.sql` w `database/migrations`. -- Pliki sa wykonywane rosnaco po nazwie. -- Wykonane migracje sa zapisywane w tabeli `migrations`. - -## Nazewnictwo plikow -- Format: `YYYYMMDD_HHMMSS_opis.sql` -- Przyklad: `20260221_000001_create_users_table.sql` - -## Uruchamianie -- CLI: `php bin/migrate.php` lub `composer migrate` -- Panel: `Ustawienia > Aktualizacja bazy danych > Wykonaj aktualizacje` - -## Kolejne migracje -1. Dodaj nowy plik SQL w `database/migrations`. -2. Wrzuc plik na serwer. -3. Uruchom aktualizacje z panelu albo z CLI. diff --git a/DOCS/ORDERS_SCHEMA_DRAFT.md b/DOCS/ORDERS_SCHEMA_DRAFT.md new file mode 100644 index 0000000..88c31f7 --- /dev/null +++ b/DOCS/ORDERS_SCHEMA_DRAFT.md @@ -0,0 +1,44 @@ +# Orders Schema Draft (Generic) + +## Context +- This is a generic schema proposal for external orders import/sync. +- API docs from Apilo were used only as an example of a rich order payload shape. +- Target is integration-agnostic storage (no vendor lock in table/column names). + +## Proposed tables +- `orders` +- `order_addresses` +- `order_items` +- `order_payments` +- `order_shipments` +- `order_documents` +- `order_notes` +- `order_status_history` +- `order_tags_dict` +- `order_tag_links` +- `integration_order_sync_state` + +SQL draft: +- [20260302_orders_schema_v1.sql](/C:/visual%20studio%20code/projekty/orderPRO/database/drafts/20260302_orders_schema_v1.sql) + +## Design principles +- Keep one row per imported source order in `orders`. +- Store child collections in dedicated tables (1:N). +- Keep source IDs and selected business scalars as first-class columns. +- Keep raw payload snapshots (`payload_json`) for diagnostics and replay safety. +- Keep import idempotent: + - unique `(integration_id, source_order_id)` in `orders`, + - unique child keys per order and source child ID. +- Keep event timeline in separate `order_status_history`. + +## Why these extra tables +- `order_status_history`: audit + timeline reconstruction + sync debugging. +- `integration_order_sync_state`: robust incremental fetch cursor. +- `order_tags_*`: proper many-to-many, easy filtering and deduplication. + +## Notes before implementation +- Draft is intentionally in `database/drafts` (not auto-run by Migrator). +- Before production migration: + - confirm source-specific mapping in service layer, + - confirm retention policy for `payload_json`, + - decide merge strategy for child rows (upsert by source IDs vs hard refresh per sync). diff --git a/DOCS/PLAN_MODULU_POWIAZAN_PRODUKTOW.md b/DOCS/PLAN_MODULU_POWIAZAN_PRODUKTOW.md deleted file mode 100644 index 5bde038..0000000 --- a/DOCS/PLAN_MODULU_POWIAZAN_PRODUKTOW.md +++ /dev/null @@ -1,235 +0,0 @@ -# orderPRO - Plan wdrozenia modulu powiazan produktow (shopPRO + marketplace) - -Data: 2026-02-23 -Status: draft do implementacji - -## Status realizacji (2026-02-23) -- Etap A: wykonany -- Etap B: wykonany -- Etap C: wykonany -- Etap D: wykonany (LinkMatcherService, priorytety EAN/SKU, endpoint sugestii) -- Etap E: w toku (UI historii zdarzen + soft-unlink i audyt gotowe; do domkniecia testy manualne E2E) - -## Ustalenie implementacyjne (2026-02-23) -- Import produktu z integracji tworzy od razu powiazanie w `product_channel_map` z ustawionym `integration_id` dla tej konkretnej instancji konta. - -## 1. Cel etapu -Zbudowac osobny modul "Powiazania ofert" pozwalajacy mapowac produkty orderPRO do ofert zewnetrznych z instancji: -- shopPRO, -- marketplace. - -Modul ma dzialac jako osobna karta (analogicznie do podejscia Apilo/BaseLinker), ale osadzona w obecnej architekturze orderPRO. - -## 2. Stan obecny i luka -### 2.1 Co juz jest -- tabele `sales_channels` oraz `product_channel_map`, -- seeding kanalow (`shoppro`, `allegro`, `erli`) w `IntegrationRepository::ensureSalesChannelsSeeded()`, -- podstawowe mapowanie `product_id + channel_code + external ids` przez `upsertProductChannelMap(...)`. - -### 2.2 Czego brakuje -- brak rozroznienia instancji kont (np. kilka kont marketplace), -- brak lokalnego cache ofert zewnetrznych do wyszukiwania i laczenia, -- brak statusow powiazania i konfliktow, -- brak dedykowanego UI "Powiazania", -- brak audytu operacji recznych (powiaz/przepnij/odlacz). - -## 3. Zakres MVP modulu powiazan -W etapie MVP wdrazamy: -1. osobna karta "Powiazania" na szczegolach produktu, -2. reczne powiazanie produktu orderPRO z oferta zewnetrzna, -3. auto-podpowiedzi po `EAN` i `SKU` (bez automatycznego zapisu), -4. odlaczanie i przepinanie powiazan, -5. podstawowy audit trail w osobnej tabeli logow, -6. import/listowanie ofert z aktywnych integracji do lokalnej tabeli cache. - -W MVP NIE wdrazamy: -- pelnej, automatycznej synchronizacji cen/stanow, -- fuzzy matching po nazwie jako auto-link, -- masowych operacji na setkach rekordow naraz. - -## 4. Model danych (docelowy dla MVP + rozszerzalny) -### 4.1 Rozszerzenie istniejacej tabeli mapowania -Tabela: `product_channel_map` (istniejaca) - -Dodac kolumny: -- `integration_id` INT UNSIGNED NULL (FK -> `integrations.id`) - wskazuje konkretna instancje konta, -- `link_type` VARCHAR(32) NOT NULL DEFAULT 'manual' (`manual`, `auto_sku`, `auto_ean`), -- `link_status` VARCHAR(32) NOT NULL DEFAULT 'active' (`active`, `conflict`, `inactive`, `unverified`), -- `confidence` TINYINT UNSIGNED NULL, -- `linked_at` DATETIME NULL, -- `linked_by_user_id` INT UNSIGNED NULL (FK -> `users.id`), -- `unlinked_at` DATETIME NULL, -- `unlinked_by_user_id` INT UNSIGNED NULL (FK -> `users.id`), -- `sync_meta_json` JSON NULL (pole techniczne pod dane z API). - -Indeksy: -- `(integration_id, external_product_id, external_variant_id)`, -- `(product_id, link_status)`, -- `(channel_id, link_status)`. - -Uwagi: -- utrzymac kompatybilnosc ze starym kodem (`upsertProductChannelMap`), -- `integration_id` moze byc NULL tylko dla rekordow historycznych. - -### 4.2 Nowa tabela cache ofert zewnetrznych -Tabela: `channel_offers` -- `id` INT UNSIGNED PK -- `integration_id` INT UNSIGNED NOT NULL FK -> `integrations.id` -- `channel_id` INT UNSIGNED NOT NULL FK -> `sales_channels.id` -- `external_product_id` VARCHAR(128) NOT NULL -- `external_variant_id` VARCHAR(128) NULL -- `external_offer_id` VARCHAR(128) NULL -- `name` VARCHAR(255) NOT NULL -- `sku` VARCHAR(128) NULL -- `ean` VARCHAR(32) NULL -- `price_brutto` DECIMAL(12,2) NULL -- `quantity` DECIMAL(12,3) NULL -- `currency` VARCHAR(8) NULL -- `offer_status` VARCHAR(32) NOT NULL DEFAULT 'active' -- `source_updated_at` DATETIME NULL -- `last_seen_at` DATETIME NOT NULL -- `payload_json` JSON NULL -- `created_at`, `updated_at` - -Unikalnosc: -- `UNIQUE (integration_id, external_product_id, external_variant_id)`. - -### 4.3 Nowa tabela logow powiazan -Tabela: `product_link_events` -- `id` INT UNSIGNED PK -- `product_channel_map_id` INT UNSIGNED NOT NULL FK -> `product_channel_map.id` -- `event_type` VARCHAR(32) NOT NULL (`linked`, `relinked`, `unlinked`, `status_changed`, `conflict_detected`) -- `before_json` JSON NULL -- `after_json` JSON NULL -- `created_by_user_id` INT UNSIGNED NULL FK -> `users.id` -- `created_at` DATETIME NOT NULL - -## 5. Architektura aplikacyjna (orderPRO) -Nowy modul: `src/Modules/ProductLinks` - -Klasy: -- `ProductLinksController` - karta "Powiazania", akcje reczne, -- `ProductLinksService` - logika powiaz/przepnij/odlacz + reguly konfliktow, -- `ProductLinksRepository` - odczyt/zapis `product_channel_map` i `product_link_events`, -- `ChannelOffersRepository` - odczyt cache ofert, -- `LinkMatcherService` - podpowiedzi po EAN/SKU, -- `OfferImportService` - import ofert z API integracji do `channel_offers`. - -Wspolpraca z istniejacymi modulami: -- `Settings/IntegrationRepository` - lista aktywnych instancji, -- `Products/ProductRepository` - dane produktu lokalnego, -- `Core/Security/Csrf` + `Auth` - autoryzacja i CSRF dla akcji POST. - -## 6. UI/UX (osobna karta) -### 6.1 Widok produktu -W `resources/views/products/show.php` dodac zakladke: -- `Powiazania`. - -Nowy widok czesciowy: -- `resources/views/products/partials/links.php`. - -### 6.2 Sekcje karty -1. Aktualne powiazania: -- instancja (`integrations.name`), -- kanal (`sales_channels.name`), -- oferta (`name`, `external_product_id`, `external_variant_id`), -- dopasowanie (`link_type`, `link_status`, `confidence`), -- akcje: `Przepnij`, `Odlacz`. - -2. Wyszukiwarka ofert: -- filtr po instancji, -- filtr po SKU/EAN/nazwie, -- lista wynikow z `channel_offers`, -- akcja `Powiaz`. - -3. Podpowiedzi automatyczne: -- sekcja "Proponowane dopasowania" (EAN/SKU), -- tylko sugestia, finalny zapis reczny przez operatora. - -### 6.3 Potwierdzenia i alerty -Krytyczne akcje (`Przepnij`, `Odlacz`) realizowac przez: -- `window.OrderProAlerts.confirm(...)`. - -Nie dodawac natywnych `alert()` i `confirm()`. - -## 7. API / routing wewnetrzny (SSR + POST) -Proponowane endpointy: -- `GET /products/{id}/links` - zwrot zawartosci karty (lub render w `show`), -- `POST /products/{id}/links` - utworzenie recznego powiazania, -- `POST /products/{id}/links/{mapId}/relink` - przepiecie na inna oferte, -- `POST /products/{id}/links/{mapId}/unlink` - odlaczenie, -- `GET /products/{id}/links/suggestions` - sugestie EAN/SKU. - -Backoffice import ofert: -- `POST /settings/integrations/{id}/offers/import` - reczny import cache, -- docelowo cron/job: `php bin/cron_import_offers.php` (po MVP). - -## 8. Reguly biznesowe -1. Jeden rekord oferty zewnetrznej (`integration_id + external_product_id + external_variant_id`) moze byc aktywnie powiazany tylko z jednym produktem orderPRO. -2. Jeden produkt orderPRO moze miec wiele powiazan (multi-channel, multi-instance). -3. Powiazanie reczne ma priorytet nad sugestiami auto-match. -4. `EAN exact` ma wyzszy priorytet sugestii niz `SKU exact`. -5. `SKU normalized` (bez spacji, myslnikow, podkreslen) jest nizszy niz `SKU exact`. -6. Konflikty nie modyfikuja automatycznie aktywnego linku - nadaja status `conflict` i wymagaja decyzji operatora. -7. Odlaczenie nie kasuje historii: rekord mapy moze przejsc w `link_status = inactive`. - -## 9. Etapy wdrozenia -## Etap A - migracje i repozytoria -- migracja rozszerzajaca `product_channel_map`, -- nowe tabele `channel_offers`, `product_link_events`, -- repozytoria i podstawowe testy manualne SQL. - -Kryterium akceptacji: -- migracje przechodza lokalnie, -- mozna zapisac i odczytac link wraz z `integration_id` i `link_status`. - -## Etap B - import i cache ofert -- adapter importu ofert z aktywnych integracji, -- upsert do `channel_offers`, -- reczny trigger importu z panelu Integracje. - -Kryterium akceptacji: -- po imporcie widoczne sa oferty w cache dla wskazanej integracji, -- kolejne importy aktualizuja rekordy bez duplikatow. - -## Etap C - UI karty Powiazania -- dodanie zakladki w szczegolach produktu, -- lista aktywnych powiazan, -- formularz recznego powiazania, -- akcje odlacz/przepnij z `OrderProAlerts.confirm`. - -Kryterium akceptacji: -- operator bez SQL moze powiazac, przepiac i odlaczyc oferte. - -## Etap D - sugestie i konflikty -- `LinkMatcherService` (EAN/SKU), -- widok sugestii, -- logika konfliktu i statusy. - -Kryterium akceptacji: -- sugestie sa widoczne i oznaczone confidence, -- konflikt nie psuje istniejacego aktywnego mapowania. - -## Etap E - hardening i audyt -- logowanie zdarzen do `product_link_events`, -- dopracowanie komunikatow i walidacji, -- testy manualne end-to-end. - -Kryterium akceptacji: -- kazda operacja reczna ma wpis w historii, -- UI poprawnie pokazuje status i ostatnia zmiane. - -## 10. Definicja "Done" dla modulu -- istnieje osobna karta "Powiazania" na produkcie, -- mozliwe jest reczne mapowanie produkt <-> oferta (shopPRO/marketplace), -- dziala odlaczanie i przepinanie z potwierdzeniem UI, -- cache ofert z integracji dziala i jest przeszukiwalny, -- konflikty sa oznaczane i nie niszcza danych, -- operacje sa audytowane. - -## 11. Kolejny krok po MVP -Po MVP uruchomic: -1. job/cron cyklicznego importu ofert, -2. automatyczne auto-linkowanie tylko dla przypadkow `confidence >= prog`, -3. masowe operacje mapowania (bulk), -4. wykorzystanie powiazan w sync cen/stanow i publikacji ofert. diff --git a/DOCS/PLAN_MODULU_PRODUKTOW.md b/DOCS/PLAN_MODULU_PRODUKTOW.md deleted file mode 100644 index ea85357..0000000 --- a/DOCS/PLAN_MODULU_PRODUKTOW.md +++ /dev/null @@ -1,289 +0,0 @@ -# orderPRO - Plan wdrozenia modulu produktow (bez importu/eksportu) - -Data: 2026-02-23 -Status: draft do implementacji - -## 1. Cel etapu -Zbudowac lokalny modul produktow w orderPRO, gotowy pod obsluge wielu kanalow sprzedazy (co najmniej 2 sklepy shopPRO), ale bez uruchamiania synchronizacji import/export. - -W tym etapie tworzymy: -- model danych produktu w orderPRO, -- panel do listowania, filtrowania, podgladu i edycji produktow, -- obsluge wariantow i atrybutow, -- podstawy pod przyszle mapowanie kanalowe (shopPRO, Allegro, Erli). - -W tym etapie NIE tworzymy: -- importu z shopPRO, -- eksportu do shopPRO, -- eksportu do marketplace, -- schedulerow/cronow synchronizacji produktow. - -## 2. Zalozenia domenowe -1. orderPRO jest source of truth dla danych produktowych po wdrozeniu tego etapu. -2. Produkt ma stabilny identyfikator lokalny oraz techniczny UUID do laczenia z integracjami. -3. Jeden produkt moze miec wiele wariantow. -4. Ceny sa przechowywane lokalnie i przeliczane dwukierunkowo (brutto <-> netto) na podstawie VAT. -5. Stan magazynowy jest przechowywany wylacznie na produkcie glownym. -6. Modul ma byc przygotowany pod wiele kanalow, ale bez aktywnych procesow synchronizacji. -7. W MVP obslugiwany jest jezyk `pl`, z zachowaniem struktury pod kolejne jezyki. - -## 3. Zakres funkcjonalny MVP modulu produktow -### 3.1 Lista produktow -- paginacja, -- wyszukiwanie po nazwie, SKU, EAN, -- filtry: status, typ (prosty/wariantowy), producent, data modyfikacji, -- sortowanie: id, nazwa, SKU, cena, stan, status, data modyfikacji, -- widoczne flagi: aktywny/nieaktywny, promowany. - -### 3.2 Szczegoly produktu -- dane glowne: nazwa, SKU, EAN, status, promoted, -- ceny: brutto/netto + promo, -- stan magazynowy i waga, -- VAT i jednostka, -- kategorie lokalne, -- opis i meta (PL jako minimum), -- galeria zdjec (metadane i kolejnosc). - -### 3.3 Tworzenie i edycja produktu -- formularz z walidacja, -- partial update, -- historia zmian (minimum: kto, kiedy, co zmienil). - -### 3.4 Warianty -- dodawanie wariantu po kombinacji atrybutow, -- walidacja unikalnosci kombinacji, -- osobne pola SKU/EAN/cena/waga/status dla wariantu (bez osobnego stanu), -- aktywacja/dezaktywacja wariantu. - -### 3.5 Atrybuty i wartosci -- slownik atrybutow i wartosci lokalnych, -- oznaczenie typu atrybutu, -- mozliwosc przypisania wielu atrybutow do produktu. - -## 4. Model danych (propozycja) -### 4.1 Tabele glówne -1. `products` -- `id` BIGINT PK -- `uuid` CHAR(36) UNIQUE -- `type` ENUM('simple','variant_parent') -- `sku` VARCHAR(128) NULL UNIQUE -- `ean` VARCHAR(32) NULL -- `status` TINYINT(1) -- `promoted` TINYINT(1) -- `vat` DECIMAL(5,2) NULL -- `weight` DECIMAL(10,3) NULL -- `price_brutto` DECIMAL(12,2) -- `price_brutto_promo` DECIMAL(12,2) NULL -- `price_netto` DECIMAL(12,2) NULL -- `price_netto_promo` DECIMAL(12,2) NULL -- `quantity` DECIMAL(12,3) DEFAULT 0 -- `producer_id` BIGINT NULL -- `product_unit_id` BIGINT NULL -- `created_at`, `updated_at`, `deleted_at` - -2. `product_translations` -- `id` BIGINT PK -- `product_id` BIGINT FK -> products.id -- `lang` VARCHAR(8) -- `name` VARCHAR(255) -- `short_description` TEXT NULL -- `description` LONGTEXT NULL -- `meta_title` VARCHAR(255) NULL -- `meta_description` VARCHAR(255) NULL -- `meta_keywords` VARCHAR(255) NULL -- `seo_link` VARCHAR(255) NULL -- UNIQUE (`product_id`, `lang`) - -3. `product_images` -- `id` BIGINT PK -- `product_id` BIGINT FK -- `storage_path` VARCHAR(255) -- `alt` VARCHAR(255) NULL -- `sort_order` INT DEFAULT 0 -- `is_main` TINYINT(1) DEFAULT 0 - -4. `product_categories` -- `product_id` BIGINT FK -- `category_id` BIGINT FK -- PK (`product_id`, `category_id`) - -5. `product_variants` -- `id` BIGINT PK -- `product_id` BIGINT FK -> products.id -- `permutation_hash` VARCHAR(191) -- `sku` VARCHAR(128) NULL UNIQUE -- `ean` VARCHAR(32) NULL -- `status` TINYINT(1) -- `price_brutto` DECIMAL(12,2) NULL -- `price_brutto_promo` DECIMAL(12,2) NULL -- `price_netto` DECIMAL(12,2) NULL -- `price_netto_promo` DECIMAL(12,2) NULL -- `weight` DECIMAL(10,3) NULL -- `created_at`, `updated_at` -- UNIQUE (`product_id`, `permutation_hash`) - -6. `product_variant_attributes` -- `variant_id` BIGINT FK -> product_variants.id -- `attribute_id` BIGINT FK -- `value_id` BIGINT FK -- PK (`variant_id`, `attribute_id`) - -7. `attributes` -- `id` BIGINT PK -- `type` TINYINT -- `status` TINYINT(1) - -8. `attribute_translations` -- `attribute_id` BIGINT FK -- `lang` VARCHAR(8) -- `name` VARCHAR(255) -- PK (`attribute_id`, `lang`) - -9. `attribute_values` -- `id` BIGINT PK -- `attribute_id` BIGINT FK -- `status` TINYINT(1) -- `is_default` TINYINT(1) -- `impact_on_price` DECIMAL(12,2) NULL - -10. `attribute_value_translations` -- `value_id` BIGINT FK -- `lang` VARCHAR(8) -- `name` VARCHAR(255) -- PK (`value_id`, `lang`) - -11. `product_change_log` -- `id` BIGINT PK -- `product_id` BIGINT FK -- `user_id` BIGINT FK -> users.id -- `change_type` VARCHAR(64) -- `before_json` JSON NULL -- `after_json` JSON NULL -- `created_at` - -### 4.2 Tabele "future-ready" pod integracje (bez aktywnej synchronizacji) -12. `sales_channels` -- `id` BIGINT PK -- `code` VARCHAR(64) UNIQUE (np. shoppro_1, shoppro_2, allegro, erli) -- `name` VARCHAR(128) -- `type` VARCHAR(64) (shoppro/marketplace) -- `status` TINYINT(1) - -13. `product_channel_map` -- `id` BIGINT PK -- `product_id` BIGINT FK -- `channel_id` BIGINT FK -- `external_product_id` VARCHAR(128) NULL -- `external_variant_id` VARCHAR(128) NULL -- `sync_state` VARCHAR(32) DEFAULT 'not_linked' -- `last_sync_at` DATETIME NULL -- UNIQUE (`product_id`, `channel_id`, `external_product_id`, `external_variant_id`) - -Uwagi: -- na tym etapie `product_channel_map` jest tylko przygotowaniem danych, -- nie tworzymy procesow, ktore aktualizuja `sync_state` automatycznie. - -## 5. Architektura aplikacyjna (orderPRO) -Nowy modul: `src/Modules/Products` - -Sugerowane klasy: -- `ProductsController` (SSR: lista, szczegoly, create, update), -- `ProductVariantsController`, -- `AttributesController`, -- `ProductRepository`, -- `VariantRepository`, -- `AttributeRepository`, -- `ProductValidator`, -- `ProductService` (transakcje i reguly domenowe), -- DTO/normalizery do mapowania formularz <-> domena. - -Widoki: -- `resources/views/products/index.php` -- `resources/views/products/form.php` -- `resources/views/products/show.php` -- `resources/views/products/partials/variants.php` -- `resources/views/products/partials/attributes.php` - -Routing (propozycja): -- `GET /products` -- `GET /products/create` -- `POST /products` -- `GET /products/{id}` -- `POST /products/{id}` (na teraz bez PUT, zgodnie z obecnym stylem formularzy) -- `POST /products/{id}/variants` -- `POST /variants/{id}` -- `GET /attributes` -- `POST /attributes` - -## 6. Walidacja i reguly biznesowe -1. Wymagane: minimum jedna nazwa tlumaczenia (`pl.name`) i `price_brutto` dla produktu prostego. -2. `sku` jest unikalne na poziomie produktu glownego (`products.sku`). -3. `sku` wariantu jest unikalne na poziomie wariantu (`product_variants.sku`) gdy jest uzupelnione. -4. Ceny brutto/netto sa liczone dwukierunkowo (na podstawie wpisanego pola i stawki VAT). -5. Wariant musi miec niepusta i unikalna kombinacje atrybutow (`permutation_hash`). -6. Stan magazynowy jest przechowywany tylko na produkcie glownym. -7. `ean` opcjonalny, ale jesli podany to walidowany formatowo. -8. Zmiany krytyczne (cena, status, stan, sku, ean) wpisywane do `product_change_log`. - -## 7. Etapy wdrozenia -## Etap A - fundament danych -- migracje tabel produktowych i atrybutowych, -- indeksy pod filtry listy, -- seed bazowych danych slownikowych (opcjonalnie). - -Kryterium akceptacji: -- migracje przechodza lokalnie i tabela status w `Settings > Aktualizacja bazy` pokazuje brak pending. - -## Etap B - backend CRUD produktu -- repozytoria + serwis + walidacja, -- endpointy/form actions dla create/edit/show/list, -- log zmian. - -Kryterium akceptacji: -- mozna utworzyc i edytowac produkt prosty, -- walidacje dzialaja, -- zmiany zapisuja sie w `product_change_log`. - -## Etap C - warianty i atrybuty -- CRUD atrybutow i wartosci, -- dodawanie/edycja wariantow, -- unikalnosc kombinacji atrybutow, -- obsluga konwersji simple <-> variant_parent. - -Kryterium akceptacji: -- mozna dodac wiele wariantow jednego produktu, -- system blokuje duplikat kombinacji, -- lista i szczegoly poprawnie prezentuja warianty. - -## Etap D - UI/UX panelu i stabilizacja -- finalne widoki i filtry, -- komunikaty flash i bledy, -- porzadkowanie tlumaczen `resources/lang/pl.php`, -- testy manualne flow end-to-end. - -Kryterium akceptacji: -- caly flow produktowy dziala bez SQL manualnego, -- menu zawiera sekcje "Produkty", -- formularze sa zgodne z istniejacym stylem aplikacji. - -## 8. Ustalenia biznesowe (2026-02-23) -1. `sku` jest unikalne na poziomie produktu glownego. -2. `sku` jest unikalne rowniez na poziomie wariantu (jesli wariant ma wypelnione SKU). -3. Ceny netto/brutto sa wyliczane automatycznie w obie strony. -4. Stan magazynowy jest tylko na produkcie glownym. -5. MVP dziala na jezyku `pl`, ale struktura danych zostaje wielojezyczna. -6. Kategorie w tym etapie tylko jako relacja (`product_categories`), bez CRUD kategorii. -7. Zdjecia sa przechowywane na serwerze orderPRO. -8. Dozwolona jest konwersja produktu prostego na wariantowy i odwrotnie. -9. Uprawnienia szczegolowe do modulu produktow sa odlozone (dostep dla zalogowanych). - -## 9. Definicja "Done" dla tego etapu -- modul produktow dostepny z panelu, -- kompletne CRUD dla produktu prostego, -- obsluga wariantow i atrybutow, -- dzialajace filtrowanie i paginacja, -- log zmian, -- przygotowane mapowanie kanalowe (tabele), ale bez aktywnej synchronizacji. - -## 10. Co dalej po tym etapie -Kolejny etap to osobny plan: import produktow z 2x shopPRO + export z orderPRO do shopPRO/Allegro/Erli z kolejka, retry i monitorowaniem sync. diff --git a/DOCS/PLAN_PROJEKTU.md b/DOCS/PLAN_PROJEKTU.md deleted file mode 100644 index 448234a..0000000 --- a/DOCS/PLAN_PROJEKTU.md +++ /dev/null @@ -1,213 +0,0 @@ -# orderPRO - Plan projektu (MVP) - -Data: 2026-02-19 - -## 0. Stan aktualny (wdrozone) -- dziala logowanie/wylogowanie z sesja, middleware i CSRF, -- dziala routing HTTP i renderowanie widokow SSR (wlasny lekki rdzen), -- jest bazowy panel (`topbar + content`) oraz ekran logowania, -- teksty interfejsu zostaly przeniesione do tlumaczen (`resources/lang/pl.php`), -- style sa utrzymywane w SCSS (`resources/scss`) i kompilowane/minifikowane do `public/assets/css`, -- wyglad loginu i panelu zostal ujednolicony pod styl adsPRO (kolory, typografia, buttony, spacing). - -## 1. Cel -Zbudowac uproszczony panel do obslugi zamowien (inspiracja: Baselinker/Apilo/Sellasist), dzialajacy na hostingu wspoldzielonym (DirectAdmin), bez pelnego frameworka PHP. - -Zakres MVP: -- import zamowien z wlasnego sklepu i Allegro, -- obsluga zamowien w panelu (statusy, notatki), -- tworzenie przesylek przez Apaczka (recznie przez przyciski), -- integracja z zewnetrznym systemem faktur/paragonow, -- uproszczona synchronizacja stanow magazynowych. - -## 2. Zalozenia techniczne -- PHP: 8.4 -- Baza: MySQL 8.x -- Hosting: wspoldzielony (DirectAdmin), cron dostepny -- Skala startowa: do 50 zamowien dziennie -- Model: jedna firma (single-tenant) -- Styl wdrozenia: szybkie MVP - -## 3. Architektura (bez pelnego frameworka) -Podejscie: modularny monolit + lekki wlasny rdzen. - -Elementy: -- Router HTTP (wlasny) -- Kontrolery + serwisy domenowe -- Warstwa dostepu do danych (repozytoria) -- Widoki SSR (natywne PHP templates) -- Integracje przez adaptery API (Guzzle) -- Kolejka oparta o MySQL (`jobs`) uruchamiana z crona - -Zalecane biblioteki Composer: -- obecnie brak dodatkowych bibliotek runtime (rdzen jest autorski), -- biblioteki integracyjne/testowe beda dodawane etapowo, gdy pojawia sie realne moduly integracji. - -## 4. Struktura katalogow -```txt -orderPRO/ - public/ - index.php - assets/ - css/ - js/ - img/ - bootstrap/ - app.php - container.php - config/ - app.php - database.php - queue.php - integrations.php - src/ - Core/ - Http/ - Routing/ - Security/ - Database/ - Queue/ - View/ - Support/ - Modules/ - Auth/ - Dashboard/ - Orders/ - Inventory/ - Shipping/ - Documents/ - Integrations/ - Store/ - Allegro/ - Apaczka/ - Billing/ - database/ - migrations/ - seeders/ - storage/ - logs/ - cache/ - sessions/ - tmp/ - bin/ - cron_sync.php - cron_queue.php - cron_tracking.php - tests/ - Unit/ - Feature/ - DOCS/ - PLAN_PROJEKTU.md - BACKLOG_MIKROZADANIA.md -``` - -## 5. Glowne moduly MVP -1. Auth -- logowanie i wylogowanie -- reset hasla -- ochrona sesji i CSRF - -2. Orders -- lista zamowien (filtry, statusy) -- szczegoly zamowienia -- notatki i historia zmian statusu - -3. Integrations -- wlasny sklep: import zamowien -- Allegro: OAuth + import zamowien -- Apaczka: utworzenie przesylki i pobranie etykiety -- Billing: tworzenie dokumentow w zewnetrznym systemie - -4. Inventory (uproszczone) -- magazyn logiczny (SKU, ilosc) -- rezerwacja przy zamowieniu -- zwolnienie/anulowanie rezerwacji - -5. Jobs + Cron -- przetwarzanie zadan asynchronicznych -- retry i log bledow - -## 6. Model danych (MVP) -Tabele: -- `users` -- `channels` (konfiguracje API: store/allegro/apaczka/billing) -- `orders` -- `order_items` -- `order_addresses` -- `order_status_history` -- `shipments` -- `shipment_labels` -- `documents` (ID/link w systemie zewnetrznym) -- `inventory_items` -- `inventory_reservations` -- `jobs` -- `failed_jobs` -- `sync_runs` -- `sync_errors` -- `webhook_events` (na przyszlosc) - -## 7. Synchronizacja i cron -Konfiguracja na shared hostingu: -- co 5 min: `php bin/cron_sync.php` (import zamowien) -- co 1-2 min: `php bin/cron_queue.php` (obsluga jobow) -- co 15 min: `php bin/cron_tracking.php` (statusy przesylek) - -Zasady: -- krotkie zadania (batch np. po 20-50 rekordow), -- timeouty i retry z backoff, -- logowanie kazdej proby integracji. - -## 8. Integracje (kolejnosc) -1. Wlasny sklep (pierwszy connector) -2. Allegro (drugi connector) -3. Apaczka (tworzenie przesylki z przycisku) -4. System billingowy (faktura/paragon przez API) - -## 9. Plan etapow -## Etap 0 - Fundament -- skeleton aplikacji -- routing, DI/container, konfiguracja `.env` -- baza i migracje -- logowanie uzytkownika -- layout panelu admin - -## Etap 1 - Orders + wlasny sklep -- import zamowien -- lista i szczegoly -- reczna zmiana statusow - -## Etap 2 - Allegro -- autoryzacja OAuth -- pobieranie i mapowanie zamowien - -## Etap 3 - Apaczka -- tworzenie przesylki -- etykieta PDF -- zapis numeru tracking - -## Etap 4 - Billing + stock -- wystawienie dokumentu przez API -- uproszczony stock sync - -## Etap 5 - Stabilizacja -- retry, logi, obsluga bledow -- testy kluczowych flow -- backup i checklista produkcyjna - -## 10. Wymagania niefunkcjonalne -- bezpieczenstwo: - - hashowanie hasel (`password_hash`) - - CSRF tokeny - - walidacja wejscia - - escaping danych w widokach -- audyt: - - logi dzialan uzytkownika i integracji -- wydajnosc: - - paginacja list - - indeksy DB pod filtry zamowien i statusow - -## 11. Decyzje odlozone na pozniej -- multi-tenant (wiele firm) -- zaawansowana dokumentacja magazynowa (WZ/PZ) -- automatyka regul biznesowych -- migracja na VPS / Docker diff --git a/DOCS/TECH_CHANGELOG.md b/DOCS/TECH_CHANGELOG.md new file mode 100644 index 0000000..1299d53 --- /dev/null +++ b/DOCS/TECH_CHANGELOG.md @@ -0,0 +1,131 @@ +# Tech Changelog + +## 2026-03-02 +- Dodano zakladke `Ustawienia > Statusy` do zarzadzania: + - grupami statusow (z kolorem na poziomie grupy), + - statusami przypisanymi do grup. +- Dodano migracje `20260302_000022_create_order_status_groups_and_statuses_tables.sql`: + - tabela `order_status_groups`, + - tabela `order_statuses` z FK `order_statuses_group_fk` i kasowaniem kaskadowym. +- Dodano `App\Modules\Settings\OrderStatusRepository` (CRUD grup/statusow i walidacja unikalnosci kodow). +- Rozszerzono `App\Modules\Settings\SettingsController` o endpointy: + - `statuses`, + - `createStatusGroup`, `updateStatusGroup`, `deleteStatusGroup`, + - `createStatus`, `updateStatus`, `deleteStatus`. +- Rozszerzono routing o trasy `/settings/statuses*` i `/settings/status-groups*`. +- Sidebar ustawien ma nowy link `Statusy`. +- Dodano widok `resources/views/settings/statuses.php` oraz style SCSS dla formularzy/akcji tego widoku. +- Potwierdzenia usuwania w nowym widoku realizowane sa przez `window.OrderProAlerts.confirm(...)`. +- Przebudowano UI `Ustawienia > Statusy`: + - 2 taby (`Statusy`, `Grupy statusow`), + - sortowanie realizowane przez drag-and-drop z automatycznym zapisem kolejnosci po upuszczeniu. +- Skondensowano UI zakladki `Ustawienia > Statusy`: + - elementy listy statusow i grup maja bardziej kompaktowy, jednoliniowy uklad, + - zmniejszono paddingi/gapy i wysokosci kontrolek, aby zwiekszyc ilosc danych widocznych bez scrolla. +- Wprowadzono globalna preferencje kompaktowego UI w `AGENTS.md`. +- Poprawiono generowanie `code` dla statusow/grup: polskie znaki sa transliterowane do ASCII + (np. `Nieopłacone` -> `nieoplacone`), zamiast zamiany na `_`. +- Dodano skrypt serwisowy `bin/fix_status_codes.php`: + - przelicza kody grup/statusow na podstawie aktualnych nazw z transliteracja PL->ASCII, + - zapewnia unikalnosc kodow (`_2`, `_3` przy konfliktach), + - wspiera `--dry-run` i `--use-remote`. +- Wykonano naprawe kodow na bazie zdalnej (`--use-remote`): zaktualizowano 2 grupy i 1 status. +- Przygotowano draft generycznego schematu tabel zamowien (Apilo tylko jako przyklad pol API): + - dokumentacja: `DOCS/ORDERS_SCHEMA_DRAFT.md`, + - draft SQL (nieuruchamiany automatycznie): `database/drafts/20260302_orders_schema_v1.sql`. +- Wdrozono generyczny schema zamowien na bazie docelowej przez `bin/deploy_and_seed_orders.php`. +- Zasiano dane testowe: + - `orders`: 30, + - `order_items`: 90, + - `order_status_history`: 123, + - pozostale kolekcje (adresy/platnosci/wysylki/dokumenty/notatki/tagi) proporcjonalnie. +- Dodano endpointy zapisu kolejnosci: + - `POST /settings/status-groups/reorder`, + - `POST /settings/statuses/reorder`. +- Zmieniono obsluge pola `code`: + - `code` jest automatycznie generowany przy tworzeniu z nazwy, + - po utworzeniu jest tylko do odczytu i nie podlega edycji z formularza. +- Reset projektu do trybu `users-only`. +- Zarchiwizowano moduly poza `Auth` i `Users` do `archive/2026-03-02_users-only-reset/`. +- Uproszczono routing i layout do obslugi logowania i zarzadzania uzytkownikami. +- Ustalono nowy standard dokumentacji technicznej w plikach root: + - `DB_SCHEMA.md` + - `ARCHITECTURE.md` + - `TECH_CHANGELOG.md` +- Przywrocono sekcje `Ustawienia` w nawigacji jako grupe z podkategoriami: + - `Uzytkownicy` (`/users`) + - `Baza danych` (`/settings/database`) +- Dodano modul `App\Modules\Settings` z kontrolerem `SettingsController` (metody `database`, `migrate`). +- Przywrocono reczne uruchamianie migracji z UI: + - `GET /settings/database` (status migracji + lista pending plikow), + - `POST /settings/database/migrate` (wykonanie pending migracji + log ostatniego uruchomienia). +- Zmieniono tlumaczenie `settings.database.title` na `Baza danych` oraz dodano `navigation.database`. +- Poprawiono redirect po logowaniu (`AuthController`): `/dashboard` -> `/settings/users`. +- Usunieto wewnetrzny pasek zakladek (`settings-nav`) z podstron ustawien. +- Podstrona uzytkownikow jest adresowana jako `GET/POST /settings/users` (z zachowaniem tras kompatybilnosci `/users`). +- Usunieto z podstron ustawien blok naglowkowy `Ustawienia` + opis, aby zwiekszyc obszar roboczy. +- Rozszerzono `bin/deploy_and_seed_orders.php` o parametr `--profile=default|realistic`. +- Dodano realistyczny profil seedowania: + - wazone losowanie statusow i metod platnosci, + - spojne mapowanie `external_status_id` -> `payment_status` i `total_paid`, + - bardziej realne reguly tworzenia wpisow `order_payments`, `order_shipments`, `order_documents`, + - historia statusow oparta na logicznych sciezkach przejsc (zamiast losowych skokow). +- Wykonano ponowne wdrozenie draftu i seed z profilem realistycznym: + - komenda: `C:\xampp\php\php.exe bin/deploy_and_seed_orders.php --use-remote --count=30 --profile=realistic`, + - wynik: `orders=30`, `order_items=94`, `order_status_history=81`. +- Dodano glowna sekcje panelu `Zamowienia` z podzakladka `Lista zamowien`. +- Wdrozone endpointy: + - `GET /orders` (redirect do `/orders/list`), + - `GET /orders/list` (widok listy). +- Dodano modul aplikacyjny: + - `App\Modules\Orders\OrdersController`, + - `App\Modules\Orders\OrdersRepository`. +- Widok listy zamowien opiera sie o aktualna baze (`orders`, `order_addresses`, `order_items`, `order_shipments`, `order_documents`) i udostepnia: + - filtry (fraza, zrodlo, status, status platnosci, zakres dat), + - sortowanie i paginacje, + - kompaktowe komorki (referencje, klient, status+platnosc, pozycje, kwoty, wysylka, daty), + - skrócone statystyki (`wszystkie`, `oplacone`, `wyslane`). +- Rozszerzono liste zamowien o podglad produktow w zamowieniu: + - nazwa produktu, + - miniatura (z `order_items.media_url`, fallback bez obrazu), + - ilosc sztuk per pozycja, + - licznik dodatkowych pozycji poza limitem podgladu. +- Miniatury produktow na liscie zamowien zostaly powiekszone o 100% (uklad bardziej czytelny). +- Dodano modal podgladu zdjecia po kliknieciu miniatury produktu na liscie zamowien. +- Status w kolumnie statusow jest prezentowany jako nazwa biznesowa (np. `Nowe`, `W realizacji`) bez technicznego kodu. +- Dodano skrypt serwisowy `bin/fill_order_item_images.php` do uzupelniania pustych `order_items.media_url` + losowymi URL (`picsum.photos`) i wykonano go na bazie zdalnej (`--use-remote`, zaktualizowano 94 rekordy). +- Rozszerzono sidebar o grupe `Zamowienia` z podlinkiem `Lista zamowien`. +- Dodano widok szczegolow zamowienia: + - endpoint `GET /orders/{id}`, + - link do szczegolow po kliknieciu numeru zamowienia na liscie, + - uklad sekcji inspirowany widokiem Apilo: pozycje, dane zamowienia, platnosc/wysylka, adresy, notatki, historia. +- Dopracowano widok `GET /orders/{id}` do ukladu bardziej zblizonego do Apilo: + - lewy panel statusow z licznikami, + - prawa kolumna szczegolow z paskiem akcji i tabami sekcji, + - aktywne wyroznienie biezacego statusu zamowienia. +- Dodano taki sam lewy panel statusow na `GET /orders/list`: + - grupy statusow z licznikami, + - klikniecie statusu filtruje liste zamowien po `status`, + - kolorowe liczniki per status (info/warn/success/danger). +- Poprawiono zrodlo panelu statusow (lista + szczegoly): + - podzial na grupy i nazwy statusow sa pobierane dynamicznie z `order_status_groups` + `order_statuses`, + - kolory pochodza z `order_status_groups.color_hex`, + - dla statusow nieprzypisanych do konfiguracji dodawana jest sekcja `Pozostale`. +- Ujednolicono render panelu statusow jako jeden widget widoku: + - nowy komponent `resources/views/components/order-status-panel.php`, + - komponent jest wspolnie uzywany przez `orders/list.php` i `orders/show.php`, + - statusy w szczegolach zamowienia sa klikalne (przejscie do listy z odpowiednim filtrem). +- Dodano klikalne taby w `orders/show.php`: + - przelaczanie sekcji bez przeladowania strony (JS), + - aktywny panel `Szczegoly zamowienia`, + - pozostale panele (`Historia zmian`, `Przesylki`, `Platnosci`, `Dokumenty`) zawieraja tymczasowe puste boksy. +- Zmieniono seed zamowien (`bin/deploy_and_seed_orders.php`): + - `external_status_id` jest losowany z aktywnych statusow z tabeli `order_statuses` (zgodnie z konfiguracja w `Ustawienia > Statusy`), + - dodano fallback do listy domyslnej, jesli tabela jest pusta/niedostepna, + - profil `realistic` ma fallback reguly finansowej dla niestandardowych statusow. +- Dodano skrypt serwisowy `bin/randomize_order_statuses.php`: + - losowo podmienia `orders.external_status_id` dla juz istniejacych zamowien na aktywne statusy z `order_statuses`, + - aktualizuje tez `is_canceled_by_buyer` dla statusu `cancelled`, + - wspiera `--use-remote` i `--dry-run`. +- Wykonano podmiane statusow na bazie zdalnej (`--use-remote`): zaktualizowano 30 zamowien. diff --git a/DOCS/TODO.md b/DOCS/TODO.md deleted file mode 100644 index 91ea8a6..0000000 --- a/DOCS/TODO.md +++ /dev/null @@ -1,6 +0,0 @@ -5. Rozbudować dane o producencie o pola z shopPRO -11. Nowa zakładka ze stanami magazynowyi z inputami do szybkiego wpisania aktualnego stanu magazynowego -13. Możliwość edycji pojedynczych wartości dla integracji shopPRO -14. Możliwość wysyłania wybranych zdjęć przy eksporcie pojedynczego produktu. -16. Obsługa pola Pozwól zamawiać gdy stan 0: -17. Integracja z https://kie.ai/ \ No newline at end of file diff --git a/DOCS/plans/2026-02-27-marketplace-category-assignment-design.md b/DOCS/plans/2026-02-27-marketplace-category-assignment-design.md deleted file mode 100644 index c27b52f..0000000 --- a/DOCS/plans/2026-02-27-marketplace-category-assignment-design.md +++ /dev/null @@ -1,92 +0,0 @@ -# Design: Przypisywanie kategorii shopPRO z poziomu Marketplace orderPRO - -**Data:** 2026-02-27 -**Status:** Zatwierdzony - -## Cel - -Umożliwienie przypisywania produktów do kategorii instancji shopPRO bezpośrednio z widoku "Powiązane oferty" w orderPRO, bez konieczności logowania się do panelu shopPRO. - -## Architektura - -``` -Przeglądarka → orderPRO (proxy AJAX) → shopPRO API -``` - -orderPRO działa jako bezpieczny proxy — klucz API shopPRO nigdy nie trafia do przeglądarki. - -## Zakres zmian - -### shopPRO (2 pliki) - -**1. `autoload/api/Controllers/CategoriesApiController.php`** (nowy) -- Akcja `list` (GET) — zwraca płaską listę **aktywnych** kategorii: - ```json - {"status": "ok", "data": {"categories": [{"id": 1, "parent_id": null, "title": "Nazwa"}]}} - ``` -- Tytuł z `pp_shop_categories_langs` w domyślnym języku sklepu (`pp_langs` WHERE `start=1`) -- Tylko `status=1` - -**2. `autoload/api/ApiRouter.php`** -- Rejestracja endpointu `'categories'` → `CategoriesApiController` - -### orderPRO (4 pliki) - -**1. `src/Modules/Settings/ShopProClient.php`** -- Nowa metoda `fetchCategories(baseUrl, apiKey, timeoutSeconds)`: - ``` - GET api.php?endpoint=categories&action=list - Zwraca: array{ok:bool, categories:array, message:string} - ``` - -**2. `src/Modules/Marketplace/MarketplaceController.php`** -- `categoriesJson(Request)` → `GET /marketplace/{id}/categories` - - Sprawdza czy integracja jest aktywna i typu `shoppro` - - Wywołuje `ShopProClient::fetchCategories()` - - Zwraca JSON z listą kategorii -- `saveProductCategoriesJson(Request)` → `POST /marketplace/{id}/product/{pid}/categories` - - Waliduje CSRF - - Pobiera `category_ids[]` z body - - Wywołuje `ShopProClient::updateProduct()` z `{"categories": [...]}` - - Zwraca JSON sukces/błąd - -**3. `routes/web.php`** -``` -GET /marketplace/{integration_id}/categories → categoriesJson -POST /marketplace/{integration_id}/product/{pid}/categories → saveProductCategoriesJson -``` - -**4. `resources/views/marketplace/offers.php`** -- Nowa kolumna "Kategorie" (tylko gdy `integration.type === 'shoppro'`) -- Przycisk "Przypisz kategorie" z `data-product-id="{external_product_id}"` -- Modal z drzewkiem kategorii (checkbox tree, vanilla JS) -- Aktualne kategorie produktu pobierane z istniejącego `fetchProductById()` przez nowy endpoint - -## Przepływ danych (kliknięcie przycisku) - -1. Klik "Przypisz kategorie" → spinner w przycisku -2. Równoległe AJAX GET: - - `/marketplace/{id}/categories` → lista kategorii instancji - - `/marketplace/{id}/product/{pid}/categories` → aktualne kategorie produktu - (endpoint wewnętrznie woła `products/get` na shopPRO i zwraca `categories` array) -3. JS buduje drzewo kategorii z płaskiej listy (rekurencyjnie po `parent_id`) -4. Pre-zaznacza checkboxy dla już przypisanych kategorii -5. Modal otwarty -6. Użytkownik zaznacza/odznacza, klika "Zapisz" -7. POST `/marketplace/{id}/product/{pid}/categories` z `{category_ids: [1,5], csrf_token: "..."}` -8. Toast sukcesu lub błędu - -## Bezpieczeństwo - -- Klucz API shopPRO nigdy nie opuszcza serwera orderPRO -- CSRF token wymagany przy POST -- Walidacja `integration_id` i `external_product_id` (muszą być int > 0) -- Integracja musi być aktywna i należeć do zalogowanego użytkownika (istniejąca AuthService) - -## Decyzje projektowe - -- Płaska lista kategorii (nie zagnieżdżona) — drzewo buduje JS po stronie klienta -- Vanilla JS — brak dodatkowych zależności -- Tylko aktywne kategorie (status=1) -- Tytuł w domyślnym języku sklepu -- Kategorie cacheowane w pamięci JS na czas sesji strony (jeden fetch per integration) diff --git a/DOCS/plans/2026-02-27-marketplace-category-assignment.md b/DOCS/plans/2026-02-27-marketplace-category-assignment.md deleted file mode 100644 index 389a90a..0000000 --- a/DOCS/plans/2026-02-27-marketplace-category-assignment.md +++ /dev/null @@ -1,907 +0,0 @@ -# Marketplace Category Assignment Implementation Plan - -> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. - -**Goal:** Dodaj kolumnę "Przypisz kategorie" w widoku powiązanych ofert marketplace orderPRO, która otwiera modal z drzewkiem kategorii shopPRO i umożliwia zapis wybranych kategorii do instancji shopPRO. - -**Architecture:** orderPRO działa jako proxy AJAX — przeglądarka nigdy nie widzi klucza API shopPRO. shopPRO dostaje nowy endpoint `categories/list` zwracający płaską listę aktywnych kategorii. Drzewo kategorii buduje vanilla JS po stronie klienta. Zapis kategorii używa istniejącego `products/update` w shopPRO API. - -**Tech Stack:** PHP 8.x, vanilla JS (bez dodatkowych bibliotek), `window.OrderProAlerts` (globalny, już załadowany w layoucie), `Response::json()` dla AJAX. - ---- - -## Kontekst — kluczowe pliki - -| Plik | Rola | -|------|------| -| `C:\visual studio code\projekty\shopPRO\autoload\api\ApiRouter.php` | Rejestracja endpointów shopPRO API | -| `C:\visual studio code\projekty\shopPRO\autoload\api\Controllers\ProductsApiController.php` | Wzorzec dla nowego kontrolera | -| `C:\visual studio code\projekty\shopPRO\autoload\Domain\Category\CategoryRepository.php` | Metody DB kategorii | -| `C:\visual studio code\projekty\orderPRO\src\Modules\Settings\ShopProClient.php` | Klient HTTP do shopPRO | -| `C:\visual studio code\projekty\orderPRO\src\Modules\Marketplace\MarketplaceController.php` | Kontroler do rozszerzenia | -| `C:\visual studio code\projekty\orderPRO\routes\web.php` | Trasy — dodać 2 nowe | -| `C:\visual studio code\projekty\orderPRO\resources\views\marketplace\offers.php` | Widok tabeli ofert | -| `C:\visual studio code\projekty\orderPRO\resources\lang\pl.php` | Tłumaczenia PL | - ---- - -## Task 1: Nowy endpoint `categories/list` w shopPRO - -**Files:** -- Create: `C:\visual studio code\projekty\shopPRO\autoload\api\Controllers\CategoriesApiController.php` -- Modify: `C:\visual studio code\projekty\shopPRO\autoload\api\ApiRouter.php` - -### Step 1: Utwórz `CategoriesApiController.php` - -```php -categoryRepo = $categoryRepo; - } - - public function list(): void - { - if (!ApiRouter::requireMethod('GET')) { - return; - } - - $db = $GLOBALS['mdb'] ?? null; - if (!$db) { - ApiRouter::sendError('INTERNAL_ERROR', 'Database not available', 500); - return; - } - - // Pobierz domyślny język sklepu - $defaultLang = $db->get('pp_langs', 'id', ['start' => 1]); - if (!$defaultLang) { - $defaultLang = 'pl'; - } - $defaultLang = (string)$defaultLang; - - // Pobierz wszystkie aktywne kategorie (płaska lista) - $rows = $db->select( - 'pp_shop_categories', - ['id', 'parent_id'], - [ - 'status' => 1, - 'ORDER' => ['o' => 'ASC'], - ] - ); - - if (!is_array($rows)) { - ApiRouter::sendSuccess(['categories' => []]); - return; - } - - $categories = []; - foreach ($rows as $row) { - $categoryId = (int)($row['id'] ?? 0); - if ($categoryId <= 0) { - continue; - } - - $title = $db->get('pp_shop_categories_langs', 'title', [ - 'AND' => [ - 'category_id' => $categoryId, - 'lang_id' => $defaultLang, - ], - ]); - - // Fallback: jeśli brak tłumaczenia w domyślnym języku, weź pierwsze dostępne - if (!$title) { - $title = $db->get('pp_shop_categories_langs', 'title', [ - 'category_id' => $categoryId, - 'title[!]' => '', - 'LIMIT' => 1, - ]); - } - - $parentId = $row['parent_id'] !== null ? (int)$row['parent_id'] : null; - - $categories[] = [ - 'id' => $categoryId, - 'parent_id' => $parentId, - 'title' => (string)($title ?? 'Kategoria #' . $categoryId), - ]; - } - - ApiRouter::sendSuccess(['categories' => $categories]); - } -} -``` - -### Step 2: Zarejestruj endpoint w `ApiRouter.php` - -W metodzie `getControllerFactories()` dodaj wpis `'categories'` **po** wpisie `'dictionaries'`: - -```php -'categories' => function () use ($db) { - $categoryRepo = new \Domain\Category\CategoryRepository($db); - return new Controllers\CategoriesApiController($categoryRepo); -}, -``` - -### Step 3: Przetestuj endpoint ręcznie - -Wywołaj z terminala (zastąp URL, klucz i ID instancji shopPRO): -```bash -curl -s -H "X-Api-Key: TWOJ_KLUCZ" \ - "https://INSTANCJA_SHOPPRO/api.php?endpoint=categories&action=list" -``` - -Oczekiwany wynik: -```json -{ - "status": "ok", - "data": { - "categories": [ - {"id": 1, "parent_id": null, "title": "Główna kategoria"}, - {"id": 3, "parent_id": 1, "title": "Podkategoria"} - ] - } -} -``` - -### Step 4: Commit w shopPRO - -```bash -cd "C:\visual studio code\projekty\shopPRO" -git add autoload/api/Controllers/CategoriesApiController.php autoload/api/ApiRouter.php -git commit -m "feat: add categories/list API endpoint" -``` - ---- - -## Task 2: Metoda `fetchCategories()` w `ShopProClient` - -**Files:** -- Modify: `C:\visual studio code\projekty\orderPRO\src\Modules\Settings\ShopProClient.php` - -### Step 1: Dodaj metodę po `ensureProducer()` (przed `testConnection()`) - -```php -/** - * @return array{ok:bool,http_code:int|null,message:string,categories:array>} - */ -public function fetchCategories( - string $baseUrl, - string $apiKey, - int $timeoutSeconds -): array { - $normalizedBaseUrl = rtrim(trim($baseUrl), '/'); - $endpointUrl = $normalizedBaseUrl . '/api.php?endpoint=categories&action=list'; - - $response = $this->requestJson($endpointUrl, $apiKey, $timeoutSeconds); - if (($response['ok'] ?? false) !== true) { - return [ - 'ok' => false, - 'http_code' => $response['http_code'] ?? null, - 'message' => (string) ($response['message'] ?? 'Nie mozna pobrac kategorii z shopPRO.'), - 'categories' => [], - ]; - } - - $data = is_array($response['data'] ?? null) ? $response['data'] : []; - $categories = isset($data['categories']) && is_array($data['categories']) - ? $data['categories'] - : []; - - return [ - 'ok' => true, - 'http_code' => $response['http_code'] ?? null, - 'message' => '', - 'categories' => $categories, - ]; -} -``` - -### Step 2: Commit - -```bash -cd "C:\visual studio code\projekty\orderPRO" -git add src/Modules/Settings/ShopProClient.php -git commit -m "feat: add ShopProClient::fetchCategories() method" -``` - ---- - -## Task 3: Dwa nowe endpointy AJAX w `MarketplaceController` - -**Files:** -- Modify: `C:\visual studio code\projekty\orderPRO\src\Modules\Marketplace\MarketplaceController.php` - -### Kontekst — co robi kontroler - -Kontroler dostaje z konstruktora: `$template`, `$translator`, `$auth`, `$marketplace` (MarketplaceRepository). -Nie ma `$shopProClient` ani `$integrationRepository` — musimy je dodać przez `IntegrationRepository`. - -Spójrz jak `routes/web.php` tworzy kontroler (linia 89-94) — musimy tam dodać `ShopProClient` i `IntegrationRepository`. - -### Step 1: Rozszerz konstruktor kontrolera - -Zmień sygnaturę konstruktora na: - -```php -public function __construct( - private readonly Template $template, - private readonly Translator $translator, - private readonly AuthService $auth, - private readonly MarketplaceRepository $marketplace, - private readonly \App\Modules\Settings\IntegrationRepository $integrationRepository, - private readonly \App\Modules\Settings\ShopProClient $shopProClient -) { -} -``` - -### Step 2: Dodaj metodę `categoriesJson()` - -Uwaga: używamy `findApiCredentials()` (nie `findById()`) — tylko ta metoda zwraca odszyfrowany `api_key`. - -```php -public function categoriesJson(Request $request): Response -{ - $integrationId = max(0, (int) $request->input('integration_id', 0)); - if ($integrationId <= 0) { - return Response::json(['ok' => false, 'message' => 'Brak integration_id.'], 400); - } - - $integration = $this->marketplace->findActiveIntegrationById($integrationId); - if ($integration === null) { - return Response::json(['ok' => false, 'message' => 'Integracja nie istnieje lub jest nieaktywna.'], 404); - } - - $creds = $this->integrationRepository->findApiCredentials($integrationId); - if ($creds === null) { - return Response::json(['ok' => false, 'message' => 'Brak danych uwierzytelniających.'], 404); - } - - $result = $this->shopProClient->fetchCategories( - (string) ($creds['base_url'] ?? ''), - (string) ($creds['api_key'] ?? ''), - (int) ($creds['timeout_seconds'] ?? 10) - ); - - if (!($result['ok'] ?? false)) { - return Response::json(['ok' => false, 'message' => $result['message']], 502); - } - - return Response::json(['ok' => true, 'categories' => $result['categories']]); -} -``` - -### Step 3: Dodaj metodę `saveProductCategoriesJson()` - -Uwaga: Request::capture() buduje z `$_POST` — dla JSON body potrzebujemy `file_get_contents('php://input')`. Parsujemy ręcznie. - -```php -public function saveProductCategoriesJson(Request $request): Response -{ - $integrationId = max(0, (int) $request->input('integration_id', 0)); - $externalProductId = max(0, (int) $request->input('external_product_id', 0)); - - if ($integrationId <= 0 || $externalProductId <= 0) { - return Response::json(['ok' => false, 'message' => 'Brak wymaganych parametrów.'], 400); - } - - // CSRF z JSON body - $rawBody = (string) file_get_contents('php://input'); - $body = json_decode($rawBody, true); - if (!is_array($body)) { - return Response::json(['ok' => false, 'message' => 'Nieprawidłowe ciało żądania JSON.'], 400); - } - - $csrfToken = (string) ($body['_token'] ?? ''); - if (!\App\Core\Security\Csrf::validate($csrfToken)) { - return Response::json(['ok' => false, 'message' => 'Nieprawidłowy token CSRF.'], 403); - } - - $integration = $this->marketplace->findActiveIntegrationById($integrationId); - if ($integration === null) { - return Response::json(['ok' => false, 'message' => 'Integracja nie istnieje lub jest nieaktywna.'], 404); - } - - $creds = $this->integrationRepository->findApiCredentials($integrationId); - if ($creds === null) { - return Response::json(['ok' => false, 'message' => 'Brak danych uwierzytelniających.'], 404); - } - - $categoryIds = isset($body['category_ids']) && is_array($body['category_ids']) - ? array_values(array_filter(array_map('intval', $body['category_ids']), static fn(int $id): bool => $id > 0)) - : []; - - $result = $this->shopProClient->updateProduct( - (string) ($creds['base_url'] ?? ''), - (string) ($creds['api_key'] ?? ''), - (int) ($creds['timeout_seconds'] ?? 10), - $externalProductId, - ['categories' => $categoryIds] - ); - - if (!($result['ok'] ?? false)) { - return Response::json(['ok' => false, 'message' => $result['message']], 502); - } - - return Response::json(['ok' => true]); -} -``` - -### Step 4: Metoda `IntegrationRepository::findApiCredentials()` — potwierdzone - -Użyj `findApiCredentials(int $id): ?array` — zwraca `['id', 'name', 'base_url', 'timeout_seconds', 'api_key']` z odszyfrowanym kluczem. **Nie używaj `findById()`** — ta metoda nie zwraca klucza API. - ---- - -## Task 4: Zaktualizuj `routes/web.php` — konstruktor + 2 nowe trasy - -**Files:** -- Modify: `C:\visual studio code\projekty\orderPRO\routes\web.php` - -### Step 1: Dodaj `IntegrationRepository` i `ShopProClient` do konstruktora kontrolera - -Znajdź blok tworzenia `$marketplaceController` (linia ~89): -```php -$marketplaceController = new MarketplaceController( - $template, - $translator, - $auth, - $marketplaceRepository -); -``` - -Zastąp: -```php -$marketplaceController = new MarketplaceController( - $template, - $translator, - $auth, - $marketplaceRepository, - $integrationRepository, - $shopProClient -); -``` - -### Step 2: Dodaj 2 nowe trasy po linii z `/marketplace/{integration_id}` - -```php -$router->get('/marketplace/{integration_id}/categories', [$marketplaceController, 'categoriesJson'], [$authMiddleware]); -$router->post('/marketplace/{integration_id}/product/{external_product_id}/categories', [$marketplaceController, 'saveProductCategoriesJson'], [$authMiddleware]); -``` - -### Step 3: Sprawdź czy router obsługuje parametry w środku ścieżki - -Jeśli router nie obsługuje `/marketplace/{id}/product/{pid}/categories` (dwa parametry), użyj query stringa dla `external_product_id`: - -``` -POST /marketplace/{integration_id}/product-categories?external_product_id={pid} -``` - -I odpowiednio zaktualizuj metodę kontrolera i JS. Sprawdź jak router obsługuje routing w `src/Core/Router.php`. - -### Step 4: Commit - -```bash -git add routes/web.php src/Modules/Marketplace/MarketplaceController.php -git commit -m "feat: add AJAX category endpoints to MarketplaceController" -``` - ---- - -## Task 5: Sprawdź `IntegrationRepository::findById()` - -**Files:** -- Read: `C:\visual studio code\projekty\orderPRO\src\Modules\Settings\IntegrationRepository.php` - -Otwórz plik i potwierdź: -1. Nazwa metody zwracającej pełne dane integracji po ID (z odszyfrowanym `api_key`, `base_url`, `timeout_seconds`) -2. Zaktualizuj wywołania w `MarketplaceController` jeśli nazwa jest inna niż `findById()` - -Typowe warianty nazwy: `findById()`, `findByIdDecrypted()`, `getById()`, `findWithCredentials()`. - ---- - -## Task 6: Zaktualizuj widok `offers.php` — kolumna + modal + JS - -**Files:** -- Modify: `C:\visual studio code\projekty\orderPRO\resources\views\marketplace\offers.php` - -### Step 1: Dodaj nagłówek kolumny w `` - -Po ostatnim `` (Ostatnia zmiana), dodaj: -```php -Kategorie -``` - -### Step 2: Dodaj komórkę z przyciskiem w każdym wierszu `` - -Po ostatniej komórce `` (`updated_at`), dodaj: -```php - - - -``` - -Gdzie `$integrationId` to zmienna dostępna z danych integracji. Pobierz ją z `$integrationData['id']` na początku widoku: -```php - -``` - -### Step 3: Dodaj modal HTML na końcu widoku (przed zamknięciem ``) - -```php - - -``` - -### Step 4: Dodaj ` -``` - -**Uwaga do `productCategoriesPromise`:** Aktualny endpoint `GET /marketplace/{id}/categories` zwraca listę wszystkich kategorii. Potrzebujemy osobnego endpointu dla aktualnych kategorii produktu ALBO możemy użyć query stringa by wskazać produkt. Patrz Task 7 poniżej. - -### Step 5: Commit - -```bash -git add resources/views/marketplace/offers.php -git commit -m "feat: add category assignment column and modal to marketplace offers view" -``` - ---- - -## Task 7: Trzeci endpoint AJAX — aktualne kategorie produktu - -W kroku Task 3 mamy 2 endpointy. Potrzebujemy trzeciego do pobierania aktualnych kategorii produktu z shopPRO. - -**Files:** -- Modify: `C:\visual studio code\projekty\orderPRO\src\Modules\Marketplace\MarketplaceController.php` -- Modify: `C:\visual studio code\projekty\orderPRO\routes\web.php` - -### Step 1: Dodaj metodę `productCategoriesJson()` - -```php -public function productCategoriesJson(Request $request): Response -{ - $integrationId = max(0, (int) $request->input('integration_id', 0)); - $externalProductId = max(0, (int) $request->input('external_product_id', 0)); - - if ($integrationId <= 0 || $externalProductId <= 0) { - return Response::json(['ok' => false, 'message' => 'Brak wymaganych parametrów.'], 400); - } - - $integration = $this->marketplace->findActiveIntegrationById($integrationId); - if ($integration === null) { - return Response::json(['ok' => false, 'message' => 'Integracja nie istnieje.'], 404); - } - - $creds = $this->integrationRepository->findById($integrationId); - if ($creds === null) { - return Response::json(['ok' => false, 'message' => 'Brak danych uwierzytelniających.'], 404); - } - - $result = $this->shopProClient->fetchProductById( - (string) ($creds['base_url'] ?? ''), - (string) ($creds['api_key'] ?? ''), - (int) ($creds['timeout_seconds'] ?? 10), - $externalProductId - ); - - if (!($result['ok'] ?? false)) { - return Response::json(['ok' => false, 'message' => $result['message']], 502); - } - - $product = is_array($result['product'] ?? null) ? $result['product'] : []; - $categoryIds = isset($product['categories']) && is_array($product['categories']) - ? array_values(array_filter(array_map('intval', $product['categories']), static fn(int $id): bool => $id > 0)) - : []; - - return Response::json(['ok' => true, 'current_category_ids' => $categoryIds]); -} -``` - -### Step 2: Dodaj trasę w `routes/web.php` - -```php -$router->get('/marketplace/{integration_id}/product/{external_product_id}/categories', [$marketplaceController, 'productCategoriesJson'], [$authMiddleware]); -``` - -Jeśli router nie obsługuje dwóch parametrów w środku ścieżki, użyj: -```php -$router->get('/marketplace/{integration_id}/product-categories', [$marketplaceController, 'productCategoriesJson'], [$authMiddleware]); -``` - -I zaktualizuj URL w JS (`productCategoriesPromise`) odpowiednio. - -### Step 3: Zaktualizuj JS w `offers.php` — URL dla `productCategoriesPromise` - -Zmień URL w fetch: -```js -var productCategoriesPromise = fetch( - '/marketplace/' + integrationId + '/product/' + productId + '/categories', - { headers: { 'Accept': 'application/json' } } -) -``` - -(lub `/product-categories?external_product_id=` jeśli router nie obsługuje dwóch parametrów) - -### Step 4: Commit - -```bash -git add src/Modules/Marketplace/MarketplaceController.php routes/web.php resources/views/marketplace/offers.php -git commit -m "feat: add productCategoriesJson endpoint and fix JS fetch URL" -``` - ---- - -## Task 8: Sprawdź router — obsługa parametrów URL - -**Files:** -- Read: `C:\visual studio code\projekty\orderPRO\src\Core\Router.php` (lub podobna ścieżka) - -Otwórz plik routera i sprawdź: -1. Jak są przetwarzane segmenty `{param}` — czy obsługuje wiele parametrów w jednej trasie -2. Jak parametry trafiają do `Request` — przez `$request->input('param_name')` czy `$request->attributes` - -Jeśli router **nie obsługuje** tras w stylu `/marketplace/{id}/product/{pid}/categories` (dwa parametry dynamic), wybierz alternatywę: -``` -GET /marketplace/{integration_id}/product-categories?external_product_id={pid} -POST /marketplace/{integration_id}/product-categories (body: {external_product_id, category_ids, _token}) -``` - -Zaktualizuj odpowiednio routing, metody kontrolera i JS. - ---- - -## Task 9: Tłumaczenia w `pl.php` - -**Files:** -- Modify: `C:\visual studio code\projekty\orderPRO\resources\lang\pl.php` - -Dodaj klucze do tablicy `'marketplace'`: - -```php -'fields' => [ - // ... istniejące ... - 'categories' => 'Kategorie', -], -'actions' => [ - // ... istniejące ... - 'assign_categories' => 'Przypisz kategorie', -], -'category_modal' => [ - 'title' => 'Przypisz kategorie', - 'loading' => 'Ładowanie kategorii...', - 'no_categories' => 'Brak dostępnych kategorii.', - 'save' => 'Zapisz', - 'cancel' => 'Anuluj', - 'saving' => 'Zapisuję...', - 'saved' => 'Kategorie zapisane.', - 'error_save' => 'Błąd zapisu.', - 'error_network' => 'Błąd sieci.', -], -``` - -### Step 2: Commit - -```bash -git add resources/lang/pl.php -git commit -m "feat: add category assignment translation keys" -``` - ---- - -## Task 10: Weryfikacja end-to-end - -### Checklist testów manualnych - -1. Otwórz `https://orderpro.projectpro.pl/marketplace/1` -2. Sprawdź czy tabela ma nową kolumnę "Kategorie" -3. Kliknij "Przypisz kategorie" przy dowolnym produkcie -4. Sprawdź: modal otwiera się, spinner "Ładowanie kategorii..." widoczny -5. Sprawdź: drzewo kategorii pojawia się z rozwijanymi gałęziami -6. Sprawdź: kategorie już przypisane do produktu są wstępnie zaznaczone -7. Zaznacz/odznacz kilka kategorii, kliknij "Zapisz" -8. Sprawdź: toast "Kategorie zapisane." pojawia się, modal zamknięty -9. Otwórz modal ponownie — sprawdź czy zaznaczone kategorie są aktualne -10. Sprawdź DevTools Network — żadna odpowiedź nie może zawierać klucza API - -### Obsługa błędów - -- Jeśli shopPRO niedostępny → modal pokazuje alert z komunikatem błędu -- Jeśli CSRF wygasł → odpowiedź 403 → toast "Nieprawidłowy token CSRF." -- Jeśli produkt nie istnieje w shopPRO → toast z błędem API - -### Commit końcowy - -```bash -cd "C:\visual studio code\projekty\orderPRO" -git add -A -git commit -m "feat: marketplace category assignment complete" -``` - ---- - -## Ważne: Kluczowe ustalenia - -- **Router** obsługuje wiele parametrów `{param}` w jednej ścieżce — trasy jak `/marketplace/{id}/product/{pid}/categories` działają -- **IntegrationRepository**: używaj `findApiCredentials(int $id)` (nie `findById()`) — tylko ta metoda zwraca odszyfrowany `api_key` -- **`productCategoriesJson()`** w Task 7 też musi używać `findApiCredentials()` diff --git a/DOCS/plans/2026-02-27-per-integration-product-content-design.md b/DOCS/plans/2026-02-27-per-integration-product-content-design.md deleted file mode 100644 index d2a7930..0000000 --- a/DOCS/plans/2026-02-27-per-integration-product-content-design.md +++ /dev/null @@ -1,73 +0,0 @@ -# Design: Per-Integration Product Content - -**Date:** 2026-02-27 -**Status:** Approved - -## Summary - -Products need separate `name`, `short_description`, and `description` for each integration. Global values in `product_translations` remain the fallback. Integration-specific overrides are stored in a new table. - -## Model - -**Global + override per integration:** -- `product_translations` stays as the global/base content (unchanged) -- New table `product_integration_translations` stores per-integration overrides -- NULL field = use global value -- When exporting to a specific integration, prefer integration-specific content, fall back to global - -## Database - -New migration file: `20260227_000014_create_product_integration_translations.sql` - -```sql -CREATE TABLE product_integration_translations ( - id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, - product_id INT UNSIGNED NOT NULL, - integration_id INT UNSIGNED NOT NULL, - name VARCHAR(255) NULL, - short_description TEXT NULL, - description LONGTEXT NULL, - created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, - updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, - UNIQUE KEY pit_product_integration_unique (product_id, integration_id), - CONSTRAINT pit_product_fk FOREIGN KEY (product_id) REFERENCES products(id) ON DELETE CASCADE, - CONSTRAINT pit_integration_fk FOREIGN KEY (integration_id) REFERENCES integrations(id) ON DELETE CASCADE -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; -``` - -Data migration: For all products currently linked to the "marianek.pl" integration via `product_channel_map`, copy `name`, `short_description`, `description` from `product_translations` to `product_integration_translations`. - -## Import Flow - -In `SettingsController::importExternalProductById`: -1. Save to `product_translations` as now (global, unchanged) -2. Additionally upsert `name`, `short_description`, `description` to `product_integration_translations` for the current `integration_id` - -## Repository - -New methods in `ProductRepository`: -- `findIntegrationTranslations(int $productId): array` — returns all per-integration translation rows for a product -- `upsertIntegrationTranslation(int $productId, int $integrationId, string|null $name, string|null $shortDescription, string|null $description): void` - -## Edit UI - -In `products/edit.php`, the Name/Short description/Description section gets tabs at the top: - -``` -[ Globalna ] [ marianek.pl ] [ inny sklep... ] -``` - -- Each tab shows: Nazwa, Krótki opis, Opis (WYSIWYG with Quill) -- "Globalna" tab = existing global fields (`name`, `short_description`, `description`) -- Integration tabs = per-integration overrides (`integration_content[{id}][name]`, etc.) -- Rest of the form (prices, SKU, images, meta) is global — no tabs - -## Controller Changes - -`ProductsController`: -- `edit` action: load active integrations + `findIntegrationTranslations($id)`, pass to view -- `update` action: process `integration_content[{id}]` array, call `upsertIntegrationTranslation` for each - -## Existing Products Migration - -One-off SQL script assigns existing product content to "marianek.pl" integration. All products in `product_channel_map` linked to the marianek.pl integration get their current `product_translations` content copied to `product_integration_translations`. diff --git a/DOCS/plans/2026-02-27-per-integration-product-content.md b/DOCS/plans/2026-02-27-per-integration-product-content.md deleted file mode 100644 index bc276a8..0000000 --- a/DOCS/plans/2026-02-27-per-integration-product-content.md +++ /dev/null @@ -1,600 +0,0 @@ -# Per-Integration Product Content Implementation Plan - -> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. - -**Goal:** Store separate `name`, `short_description`, and `description` per integration (shopPRO instance), with global `product_translations` as fallback. - -**Architecture:** New table `product_integration_translations (product_id, integration_id, name, short_description, description)` stores overrides. Import saves content to both global and per-integration tables. Edit form shows tabs: Globalna | per-integration. - -**Tech Stack:** PHP 8.4, MariaDB, vanilla JS (Quill WYSIWYG already loaded on edit page) - ---- - -### Task 1: Database migration — create table - -**Files:** -- Create: `database/migrations/20260227_000014_create_product_integration_translations.sql` - -**Step 1: Create the migration file** - -```sql -CREATE TABLE IF NOT EXISTS product_integration_translations ( - id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, - product_id INT UNSIGNED NOT NULL, - integration_id INT UNSIGNED NOT NULL, - name VARCHAR(255) NULL, - short_description TEXT NULL, - description LONGTEXT NULL, - created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, - updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, - UNIQUE KEY pit_product_integration_unique (product_id, integration_id), - KEY pit_product_idx (product_id), - KEY pit_integration_idx (integration_id), - CONSTRAINT pit_product_fk - FOREIGN KEY (product_id) REFERENCES products(id) - ON DELETE CASCADE ON UPDATE CASCADE, - CONSTRAINT pit_integration_fk - FOREIGN KEY (integration_id) REFERENCES integrations(id) - ON DELETE CASCADE ON UPDATE CASCADE -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; - --- Migrate existing products to marianek.pl integration. --- Finds the integration by name 'marianek.pl' and copies current --- product_translations content for all linked products. -INSERT INTO product_integration_translations - (product_id, integration_id, name, short_description, description, created_at, updated_at) -SELECT - pt.product_id, - i.id AS integration_id, - pt.name, - pt.short_description, - pt.description, - NOW(), - NOW() -FROM product_translations pt -INNER JOIN product_channel_map pcm ON pcm.product_id = pt.product_id -INNER JOIN integrations i ON i.id = pcm.integration_id -WHERE i.name = 'marianek.pl' - AND pt.lang = 'pl' -ON DUPLICATE KEY UPDATE - name = VALUES(name), - short_description = VALUES(short_description), - description = VALUES(description), - updated_at = VALUES(updated_at); -``` - -**Step 2: Run the migration via settings panel** - -Navigate to `/settings/database` and run pending migrations, or trigger via the app's migration runner. Verify table exists: -```sql -SHOW TABLES LIKE 'product_integration_translations'; -SELECT COUNT(*) FROM product_integration_translations; -``` - -**Step 3: Commit** - -```bash -git add database/migrations/20260227_000014_create_product_integration_translations.sql -git commit -m "feat: add product_integration_translations table and migrate marianek.pl data" -``` - ---- - -### Task 2: ProductRepository — two new methods - -**Files:** -- Modify: `src/Modules/Products/ProductRepository.php` - -**Step 1: Add `findIntegrationTranslations` method** - -Add after the `findImagesByProductId` method (around line 250): - -```php -/** - * @return array> - */ -public function findIntegrationTranslations(int $productId): array -{ - $stmt = $this->pdo->prepare( - 'SELECT pit.id, pit.product_id, pit.integration_id, - pit.name, pit.short_description, pit.description, - i.name AS integration_name - FROM product_integration_translations pit - INNER JOIN integrations i ON i.id = pit.integration_id - WHERE pit.product_id = :product_id - ORDER BY i.name ASC' - ); - $stmt->execute(['product_id' => $productId]); - $rows = $stmt->fetchAll(); - - if (!is_array($rows)) { - return []; - } - - return array_map(static fn (array $row): array => [ - 'id' => (int) ($row['id'] ?? 0), - 'product_id' => (int) ($row['product_id'] ?? 0), - 'integration_id' => (int) ($row['integration_id'] ?? 0), - 'integration_name' => (string) ($row['integration_name'] ?? ''), - 'name' => isset($row['name']) ? (string) $row['name'] : null, - 'short_description' => isset($row['short_description']) ? (string) $row['short_description'] : null, - 'description' => isset($row['description']) ? (string) $row['description'] : null, - ], $rows); -} -``` - -**Step 2: Add `upsertIntegrationTranslation` method** - -Add immediately after the method above: - -```php -public function upsertIntegrationTranslation( - int $productId, - int $integrationId, - ?string $name, - ?string $shortDescription, - ?string $description -): void { - $now = date('Y-m-d H:i:s'); - $stmt = $this->pdo->prepare( - 'INSERT INTO product_integration_translations - (product_id, integration_id, name, short_description, description, created_at, updated_at) - VALUES - (:product_id, :integration_id, :name, :short_description, :description, :created_at, :updated_at) - ON DUPLICATE KEY UPDATE - name = VALUES(name), - short_description = VALUES(short_description), - description = VALUES(description), - updated_at = VALUES(updated_at)' - ); - $stmt->execute([ - 'product_id' => $productId, - 'integration_id' => $integrationId, - 'name' => $name !== '' ? $name : null, - 'short_description' => $shortDescription !== '' ? $shortDescription : null, - 'description' => $description !== '' ? $description : null, - 'created_at' => $now, - 'updated_at' => $now, - ]); -} -``` - -**Step 3: Commit** - -```bash -git add src/Modules/Products/ProductRepository.php -git commit -m "feat: add findIntegrationTranslations and upsertIntegrationTranslation to ProductRepository" -``` - ---- - -### Task 3: SettingsController — save per-integration content on import - -**Files:** -- Modify: `src/Modules/Settings/SettingsController.php` - -The import flow is in `importExternalProductById` (line ~677). After the transaction commits (line ~783), `$savedProductId` and `$integrationId` are both set. - -**Step 1: Inject ProductRepository into SettingsController** - -Check the constructor of `SettingsController`. Add `ProductRepository` as a dependency if it is not already present. Look for the constructor and add: - -```php -use App\Modules\Products\ProductRepository; -``` - -And in the constructor parameter list: -```php -private readonly ProductRepository $products, -``` - -If `$this->products` already exists (check the constructor), skip adding it — just use the existing reference. - -**Step 2: Add upsert call after transaction commit in `importExternalProductById`** - -Locate the block after `$this->pdo->commit();` (around line 783). Add the upsert call inside the try block, before the commit: - -```php -// Save per-integration content override -if ($integrationId > 0) { - $this->products->upsertIntegrationTranslation( - $savedProductId, - $integrationId, - $normalized['translation']['name'] ?? null, - $normalized['translation']['short_description'] ?? null, - $normalized['translation']['description'] ?? null - ); -} -``` - -Place this BEFORE `$this->pdo->commit()` so it's inside the transaction. - -**Step 3: Commit** - -```bash -git add src/Modules/Settings/SettingsController.php -git commit -m "feat: save per-integration name/short_description/description on product import" -``` - ---- - -### Task 4: ProductsController — load per-integration data for edit - -**Files:** -- Modify: `src/Modules/Products/ProductsController.php` - -**Step 1: Update the `edit` action (line ~186)** - -Find the block that builds data for the edit view. Currently it passes `form`, `productImages`, etc. Add two new variables: - -```php -$activeIntegrations = $this->integrations->listByType('shoppro'); -$integrationTranslations = $this->products->findIntegrationTranslations($id); - -// Index integration translations by integration_id for easy lookup in view -$integrationTranslationsMap = []; -foreach ($integrationTranslations as $it) { - $integrationTranslationsMap[(int) $it['integration_id']] = $it; -} -``` - -Add them to the `render()` call: -```php -'activeIntegrations' => $activeIntegrations, -'integrationTranslationsMap' => $integrationTranslationsMap, -``` - -**Step 2: Commit** - -```bash -git add src/Modules/Products/ProductsController.php -git commit -m "feat: pass active integrations and per-integration translations to product edit view" -``` - ---- - -### Task 5: ProductsController — save per-integration content on update - -**Files:** -- Modify: `src/Modules/Products/ProductsController.php` - -**Step 1: Update the `update` action (line ~416)** - -After the successful `$this->service->update(...)` call (and before the redirect), add: - -```php -// Save per-integration content overrides -$integrationContent = $request->input('integration_content', []); -if (is_array($integrationContent)) { - foreach ($integrationContent as $rawIntegrationId => $content) { - $integrationId = (int) $rawIntegrationId; - if ($integrationId <= 0 || !is_array($content)) { - continue; - } - $this->products->upsertIntegrationTranslation( - $id, - $integrationId, - isset($content['name']) ? trim((string) $content['name']) : null, - isset($content['short_description']) ? trim((string) $content['short_description']) : null, - isset($content['description']) ? trim((string) $content['description']) : null - ); - } -} -``` - -Place this block AFTER the image changes block and BEFORE the success Flash/redirect. - -**Step 2: Commit** - -```bash -git add src/Modules/Products/ProductsController.php -git commit -m "feat: save per-integration content overrides on product update" -``` - ---- - -### Task 6: Edit view — content tabs UI - -**Files:** -- Modify: `resources/views/products/edit.php` - -**Step 1: Replace the static name/short_description/description fields with a tabbed section** - -Current structure (around line 20-25 for name, and lines 111-125 for descriptions): - -```php - -``` - -And: -```php -
-
-``` - -**New structure:** wrap name + short_description + description in a tabbed card. Add this BEFORE the `
` (the existing grid with SKU, EAN etc.), replacing the name field in the grid: - -Remove the `name` label from `form-grid` and create a new card section above it: - -```php - - -
-
- - - - - - -
- - -
- - -
- -
-
-
- -
- -
- -
-
-
- -
-
- - - - -
-

- Puste pole = używana wartość globalna. -

- - - -
- -
-
-
- -
- -
- -
-
-
- -
-
- -
-``` - -**Step 2: Update the Quill initialization script at the bottom of edit.php** - -The current script initializes `quillShort` and `quillDesc` for global fields. Extend it to also initialize editors for each per-integration tab, and sync all on form submit: - -```js -// --- existing global editors --- -var quillShort = new Quill('#editor-short-description', { theme: 'snow', modules: { toolbar: toolbarShort } }); -var quillDesc = new Quill('#editor-description', { theme: 'snow', modules: { toolbar: toolbarFull } }); - -if (shortInput && shortInput.value) quillShort.clipboard.dangerouslyPasteHTML(shortInput.value); -if (descInput && descInput.value) quillDesc.clipboard.dangerouslyPasteHTML(descInput.value); - -// --- per-integration editors --- -var intEditors = []; // array of {shortQuill, descQuill, shortInput, descInput} - -document.querySelectorAll('[id^="editor-int-short-"]').forEach(function(el) { - var suffix = el.id.replace('editor-int-short-', ''); - var shortEl = el; - var descEl = document.getElementById('editor-int-desc-' + suffix); - var shortInp = document.getElementById('input-int-short-' + suffix); - var descInp = document.getElementById('input-int-desc-' + suffix); - - if (!shortEl || !descEl || !shortInp || !descInp) return; - - var qShort = new Quill(shortEl, { theme: 'snow', modules: { toolbar: toolbarShort } }); - var qDesc = new Quill(descEl, { theme: 'snow', modules: { toolbar: toolbarFull } }); - - if (shortInp.value) qShort.clipboard.dangerouslyPasteHTML(shortInp.value); - if (descInp.value) qDesc.clipboard.dangerouslyPasteHTML(descInp.value); - - intEditors.push({ shortQuill: qShort, descQuill: qDesc, shortInput: shortInp, descInput: descInp }); -}); - -// --- sync all on submit --- -var form = document.querySelector('.product-form'); -if (form) { - form.addEventListener('submit', function() { - if (shortInput) shortInput.value = quillShort.root.innerHTML; - if (descInput) descInput.value = quillDesc.root.innerHTML; - intEditors.forEach(function(e) { - e.shortInput.value = e.shortQuill.root.innerHTML; - e.descInput.value = e.descQuill.root.innerHTML; - }); - }); -} -``` - -**Step 3: Commit** - -```bash -git add resources/views/products/edit.php -git commit -m "feat: add per-integration content tabs to product edit form" -``` - ---- - -### Task 7: Tab switching CSS + JS - -**Files:** -- Modify: `resources/scss/app.scss` -- JS inline in `resources/views/products/edit.php` - -**Step 1: Add tab styles to app.scss** - -```scss -.content-tabs-card { - margin-top: 0; -} - -.content-tabs-nav { - display: flex; - gap: 4px; - border-bottom: 2px solid var(--c-border); - margin-bottom: 16px; - flex-wrap: wrap; -} - -.content-tab-btn { - padding: 8px 16px; - border: none; - background: none; - cursor: pointer; - font-size: 14px; - font-weight: 500; - color: var(--c-text-muted, #6b7280); - border-bottom: 2px solid transparent; - margin-bottom: -2px; - border-radius: 4px 4px 0 0; - transition: color 0.15s, border-color 0.15s; - - &:hover { - color: var(--c-text-strong, #111827); - } - - &.is-active { - color: var(--c-primary, #2563eb); - border-bottom-color: var(--c-primary, #2563eb); - } -} - -.content-tab-panel { - display: none; - - &.is-active { - display: block; - } -} -``` - -**Step 2: Add tab-switching JS in edit.php (inline, after the Quill script)** - -```js -(function() { - var nav = document.getElementById('content-tabs-nav'); - if (!nav) return; - - nav.addEventListener('click', function(e) { - var btn = e.target.closest('.content-tab-btn'); - if (!btn) return; - - var tabId = btn.getAttribute('data-tab'); - if (!tabId) return; - - // deactivate all - nav.querySelectorAll('.content-tab-btn').forEach(function(b) { - b.classList.remove('is-active'); - }); - document.querySelectorAll('.content-tab-panel').forEach(function(p) { - p.classList.remove('is-active'); - }); - - // activate selected - btn.classList.add('is-active'); - var panel = document.getElementById('content-tab-' + tabId); - if (panel) panel.classList.add('is-active'); - }); -})(); -``` - -**Step 3: Rebuild CSS** - -```bash -cd "C:/visual studio code/projekty/orderPRO" && npm run build:css -``` - -**Step 4: Commit** - -```bash -git add resources/scss/app.scss resources/views/products/edit.php public/assets/css/app.css -git commit -m "feat: tab switching styles and JS for per-integration content" -``` - ---- - -### Task 8: Add translations key - -**Files:** -- Modify: `resources/lang/pl.php` - -**Step 1: Add `content_tabs` key under `products`** - -Find the `products` array and add: - -```php -'content_tabs' => [ - 'global' => 'Globalna', -], -``` - -**Step 2: Commit** - -```bash -git add resources/lang/pl.php -git commit -m "feat: add content_tabs translation key" -``` - ---- - -### Task 9: Manual smoke test - -1. Navigate to `/settings/database` → run pending migrations → confirm `product_integration_translations` exists and has rows for marianek.pl products -2. Navigate to `/products/edit?id=32` → confirm tabs appear: "Globalna" and "marianek.pl" -3. Switch tabs → confirm fields toggle correctly -4. Edit marianek.pl tab name/description → Save → confirm saved in DB: - ```sql - SELECT * FROM product_integration_translations WHERE product_id = 32; - ``` -5. Import a product from shopPRO → confirm row created in `product_integration_translations` -6. Verify global fields unchanged after editing integration tab diff --git a/archive/2026-03-02_users-only-reset/bin/cron.php b/archive/2026-03-02_users-only-reset/bin/cron.php new file mode 100644 index 0000000..f195b8a --- /dev/null +++ b/archive/2026-03-02_users-only-reset/bin/cron.php @@ -0,0 +1,115 @@ + $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 + ); + $offerTitlesRefreshHandler = new ShopProOfferTitlesRefreshHandler( + $integrationRepository, + $offerImportService + ); + $ordersRepository = new OrdersRepository($pdo); + $orderImportService = new OrderImportService( + $integrationRepository, + $ordersRepository, + $shopProClient, + $pdo + ); + $orderStatusMappings = new OrderStatusMappingRepository($pdo); + $orderStatusSyncService = new OrderStatusSyncService( + $integrationRepository, + $ordersRepository, + $orderStatusMappings, + $shopProClient, + $pdo + ); + $ordersImportHandler = new ShopProOrdersImportHandler($orderImportService); + $orderStatusSyncHandler = new ShopProOrderStatusSyncHandler($orderStatusSyncService); + + $processor->registerHandler(CronJobType::PRODUCT_LINKS_HEALTH_CHECK, $linksHealthCheckHandler); + $processor->registerHandler(CronJobType::SHOPPRO_ORDERS_IMPORT, $ordersImportHandler); + $processor->registerHandler(CronJobType::SHOPPRO_ORDER_STATUS_SYNC, $orderStatusSyncHandler); + $processor->registerHandler(CronJobType::SHOPPRO_OFFER_TITLES_REFRESH, $offerTitlesRefreshHandler); + + $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/resources/views/dashboard/index.php b/archive/2026-03-02_users-only-reset/resources/views/dashboard/index.php similarity index 100% rename from resources/views/dashboard/index.php rename to archive/2026-03-02_users-only-reset/resources/views/dashboard/index.php diff --git a/resources/views/marketplace/index.php b/archive/2026-03-02_users-only-reset/resources/views/marketplace/index.php similarity index 100% rename from resources/views/marketplace/index.php rename to archive/2026-03-02_users-only-reset/resources/views/marketplace/index.php diff --git a/resources/views/marketplace/offers.php b/archive/2026-03-02_users-only-reset/resources/views/marketplace/offers.php similarity index 100% rename from resources/views/marketplace/offers.php rename to archive/2026-03-02_users-only-reset/resources/views/marketplace/offers.php diff --git a/archive/2026-03-02_users-only-reset/resources/views/orders/index.php b/archive/2026-03-02_users-only-reset/resources/views/orders/index.php new file mode 100644 index 0000000..4623549 --- /dev/null +++ b/archive/2026-03-02_users-only-reset/resources/views/orders/index.php @@ -0,0 +1,28 @@ +
+
+
+
+

+

+
+
+
+ + +
+ +
+ + + +
+
+ +
+
+ + + +
diff --git a/resources/views/products/create.php b/archive/2026-03-02_users-only-reset/resources/views/products/create.php similarity index 100% rename from resources/views/products/create.php rename to archive/2026-03-02_users-only-reset/resources/views/products/create.php diff --git a/resources/views/products/edit.php b/archive/2026-03-02_users-only-reset/resources/views/products/edit.php similarity index 100% rename from resources/views/products/edit.php rename to archive/2026-03-02_users-only-reset/resources/views/products/edit.php diff --git a/resources/views/products/index.php b/archive/2026-03-02_users-only-reset/resources/views/products/index.php similarity index 100% rename from resources/views/products/index.php rename to archive/2026-03-02_users-only-reset/resources/views/products/index.php diff --git a/resources/views/products/links.php b/archive/2026-03-02_users-only-reset/resources/views/products/links.php similarity index 100% rename from resources/views/products/links.php rename to archive/2026-03-02_users-only-reset/resources/views/products/links.php diff --git a/resources/views/products/show.php b/archive/2026-03-02_users-only-reset/resources/views/products/show.php similarity index 100% rename from resources/views/products/show.php rename to archive/2026-03-02_users-only-reset/resources/views/products/show.php diff --git a/resources/views/settings/cron.php b/archive/2026-03-02_users-only-reset/resources/views/settings/cron.php similarity index 97% rename from resources/views/settings/cron.php rename to archive/2026-03-02_users-only-reset/resources/views/settings/cron.php index ed5fe85..0e5f88e 100644 --- a/resources/views/settings/cron.php +++ b/archive/2026-03-02_users-only-reset/resources/views/settings/cron.php @@ -12,6 +12,7 @@ $webLimit = max(1, (int) ($webCronLimit ?? 5));
+ +
diff --git a/archive/2026-03-02_users-only-reset/resources/views/settings/order-statuses.php b/archive/2026-03-02_users-only-reset/resources/views/settings/order-statuses.php new file mode 100644 index 0000000..3d9e1dc --- /dev/null +++ b/archive/2026-03-02_users-only-reset/resources/views/settings/order-statuses.php @@ -0,0 +1,107 @@ + + +
+

+

+ +
+ +
+

+

+ + + + + + +
+ + +
+ +
+ + 0): ?> +
+ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + +
+
+ +
+ +
+
+ +
diff --git a/resources/views/settings/products.php b/archive/2026-03-02_users-only-reset/resources/views/settings/products.php similarity index 91% rename from resources/views/settings/products.php rename to archive/2026-03-02_users-only-reset/resources/views/settings/products.php index ef7551f..2421eb0 100644 --- a/resources/views/settings/products.php +++ b/archive/2026-03-02_users-only-reset/resources/views/settings/products.php @@ -5,6 +5,7 @@