From 847fdbbf3f7a3abacc3ea789d5923a0c635342d6 Mon Sep 17 00:00:00 2001 From: Jacek Pyziak Date: Sat, 14 Feb 2026 10:43:31 +0100 Subject: [PATCH] refactor(shop-statuses): migrate to DI, restructure docs into docs/ folder (0.268) - Migrate ShopStatuses module to Domain + DI architecture - Add ShopStatusRepository, ShopStatusesController with color picker - Convert front\factory\ShopStatuses to facade - Add FormFieldType::COLOR with HTML5 color picker - Move documentation files to docs/ folder (PROJECT_STRUCTURE, REFACTORING_PLAN, CHANGELOG, FORM_EDIT_SYSTEM, TESTING, DATABASE_STRUCTURE) - Tests: 254 tests, 736 assertions Co-Authored-By: Claude Opus 4.6 --- AGENTS.md | 19 +- PROJECT_STRUCTURE.md | 505 ---------- REFACTORING_PLAN.md | 873 ------------------ admin/templates/shop-statuses/status-edit.php | 80 +- admin/templates/shop-statuses/view-list.php | 39 +- admin/templates/site/main-layout.php | 2 +- .../ShopStatus/ShopStatusRepository.php | 181 ++++ .../Controllers/ShopStatusesController.php | 260 ++++++ .../admin/Support/Forms/FormFieldRenderer.php | 30 + autoload/admin/ViewModels/Forms/FormField.php | 15 + .../admin/ViewModels/Forms/FormFieldType.php | 1 + autoload/admin/class.Site.php | 7 + .../admin/controls/class.ShopStatuses.php | 34 - autoload/admin/factory/class.Integrations.php | 2 +- autoload/admin/factory/class.ShopStatuses.php | 24 - autoload/front/factory/class.ShopStatuses.php | 10 +- docs/CHANGELOG.md | 315 +++++++ .../DATABASE_STRUCTURE.md | 17 + docs/FORM_EDIT_SYSTEM.md | 172 ++++ docs/PROJECT_STRUCTURE.md | 254 +++++ docs/REFACTORING_PLAN.md | 276 ++++++ TESTING.md => docs/TESTING.md | 21 +- .../UPDATE_INSTRUCTIONS.md | 0 .../ShopStatus/ShopStatusRepositoryTest.php | 253 +++++ .../ShopStatusesControllerTest.php | 57 ++ updates/0.20/ver_0.268.zip | Bin 0 -> 25695 bytes updates/0.20/ver_0.268_files.txt | 7 + updates/changelog.php | 11 + updates/versions.php | 2 +- 29 files changed, 1895 insertions(+), 1572 deletions(-) delete mode 100644 PROJECT_STRUCTURE.md delete mode 100644 REFACTORING_PLAN.md create mode 100644 autoload/Domain/ShopStatus/ShopStatusRepository.php create mode 100644 autoload/admin/Controllers/ShopStatusesController.php delete mode 100644 autoload/admin/controls/class.ShopStatuses.php delete mode 100644 autoload/admin/factory/class.ShopStatuses.php create mode 100644 docs/CHANGELOG.md rename DATABASE_STRUCTURE.md => docs/DATABASE_STRUCTURE.md (92%) create mode 100644 docs/FORM_EDIT_SYSTEM.md create mode 100644 docs/PROJECT_STRUCTURE.md create mode 100644 docs/REFACTORING_PLAN.md rename TESTING.md => docs/TESTING.md (90%) rename UPDATE_INSTRUCTIONS.md => docs/UPDATE_INSTRUCTIONS.md (100%) create mode 100644 tests/Unit/Domain/ShopStatus/ShopStatusRepositoryTest.php create mode 100644 tests/Unit/admin/Controllers/ShopStatusesControllerTest.php create mode 100644 updates/0.20/ver_0.268.zip create mode 100644 updates/0.20/ver_0.268_files.txt diff --git a/AGENTS.md b/AGENTS.md index 3c92d26..c81696f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -6,10 +6,12 @@ Gdy użytkownik napisze `KONIEC PRACY`, wykonaj kolejno: 1. Przeprowadzenie testów. 2. Aktualizacja dokumentacji technicznej, jeśli zmiany tego wymagają: - - `DATABASE_STRUCTURE.md` - - `PROJECT_STRUCTURE.md` - - `REFACTORING_PLAN.md` - - `TESTING.md` + - `docs/DATABASE_STRUCTURE.md` + - `docs/PROJECT_STRUCTURE.md` + - `docs/REFACTORING_PLAN.md` + - `docs/FORM_EDIT_SYSTEM.md` + - `docs/CHANGELOG.md` + - `docs/TESTING.md` 3. Przygotowanie aktualizacji (ZIP, plik z usuwanymi plikami, plik SQL jeśli wymagany). 4. Commit. 5. Push. @@ -18,10 +20,11 @@ Gdy użytkownik napisze `KONIEC PRACY`, wykonaj kolejno: Przed rozpoczęciem implementacji sprawdź aktualną zawartość: -- `DATABASE_STRUCTURE.md` -- `PROJECT_STRUCTURE.md` -- `REFACTORING_PLAN.md` -- `TESTING.md` +- `docs/DATABASE_STRUCTURE.md` +- `docs/PROJECT_STRUCTURE.md` +- `docs/REFACTORING_PLAN.md` +- `docs/CHANGELOG.md` +- `docs/TESTING.md` To ma pomóc zachować spójność zmian i dokumentacji. diff --git a/PROJECT_STRUCTURE.md b/PROJECT_STRUCTURE.md deleted file mode 100644 index 7df694b..0000000 --- a/PROJECT_STRUCTURE.md +++ /dev/null @@ -1,505 +0,0 @@ -# Struktura Projektu shopPRO - -Dokumentacja struktury projektu shopPRO do szybkiego odniesienia. - -## System Cache (Redis) - -### Klasy odpowiedzialne za cache - -#### RedisConnection -- **Plik:** `autoload/class.RedisConnection.php` -- **Opis:** Singleton zarządzający połączeniem z Redis -- **Metody:** - - `getInstance()` - pobiera instancję połączenia - - `getConnection()` - zwraca obiekt Redis - -#### CacheHandler -- **Plik:** `autoload/class.CacheHandler.php` -- **Opis:** Handler do obsługi cache Redis -- **Metody:** - - `get($key)` - pobiera wartość z cache - - `set($key, $value, $ttl = 86400)` - zapisuje wartość do cache - - `exists($key)` - sprawdza czy klucz istnieje - - `delete($key)` - usuwa pojedynczy klucz - - `deletePattern($pattern)` - usuwa klucze według wzorca - -#### Klasa S (pomocnicza) -- **Plik:** `autoload/class.S.php` -- **Metody cache:** - - `clear_redis_cache()` - czyści cały cache Redis (flushAll) - - `clear_product_cache(int $product_id)` - czyści cache konkretnego produktu - -### Wzorce kluczy Redis - -#### Produkty -``` -shop\product:{product_id}:{lang_id}:{permutation_hash} -``` -- Przechowuje zserializowany obiekt produktu -- TTL: 24 godziny (86400 sekund) -- Klasa: `shop\Product::getFromCache()` - `autoload/shop/class.Product.php:121` - -#### Opcje ilościowe produktu -``` -\shop\Product::get_product_permutation_quantity_options:{product_id}:{permutation} -``` -- Przechowuje informacje o ilości i komunikatach magazynowych -- Klasa: `shop\Product::get_product_permutation_quantity_options()` - `autoload/shop/class.Product.php:549` - -#### Zestawy produktów -``` -\shop\Product::product_sets_when_add_to_basket:{product_id} -``` -- Przechowuje produkty często kupowane razem -- Klasa: `shop\Product::product_sets_when_add_to_basket()` - `autoload/shop/class.Product.php:316` - -## Integracje z systemami zewnętrznymi (CRON) - -### Plik: `cron.php` - -#### Apilo -- **Aktualizacja pojedynczego produktu:** synchronizacja cen i stanow - - Czestotliwosc: Co 10 minut -- **Synchronizacja cennika:** masowa aktualizacja cen z Apilo - - Czestotliwosc: Co 1 godzine - -**Uwaga:** Integracje Sellasist i Baselinker zostaly usuniete w ver. 0.263. - -## Panel Administratora - -### Routing -- Główny katalog: `admin/` -- Template główny: `admin/templates/site/main-layout.php` -- Kontrolery (nowe): `autoload/admin/Controllers/` -- Kontrolery legacy (fallback): `autoload/admin/controls/` - -### Przycisk "Wyczyść cache" -- **Lokalizacja UI:** `admin/templates/site/main-layout.php:172` -- **JavaScript:** `admin/templates/site/main-layout.php:235-274` -- **Endpoint AJAX:** `/admin/settings/clear_cache_ajax/` -- **Kontroler:** `autoload/admin/Controllers/SettingsController.php:43-60` -- **Działanie:** - 1. Pokazuje spinner "Czyszczę cache..." - 2. Czyści katalogi: `temp/`, `thumbs/` - 3. Wykonuje `flushAll()` na Redis - 4. Pokazuje "Cache wyczyszczony!" przez 2 sekundy - 5. Przywraca stan początkowy - -## Struktura katalogów - -``` -shopPRO/ -├── admin/ # Panel administratora -│ ├── templates/ # Szablony widoków -│ └── layout/ # Zasoby CSS/JS/ikony -├── autoload/ # Klasy autoloadowane -│ ├── admin/ # Klasy panelu admin -│ │ ├── controls/ # Kontrolery -│ │ └── factory/ # Fabryki/helpery -│ ├── front/ # Klasy frontendu -│ │ └── factory/ # Fabryki/helpery -│ └── shop/ # Klasy sklepu -├── libraries/ # Biblioteki zewnętrzne -├── temp/ # Cache tymczasowy -├── thumbs/ # Miniatury zdjęć -└── cron.php # Zadania CRON - -``` - -## Baza danych - -### Główne tabele produktów -- `pp_shop_products` - produkty główne -- `pp_shop_products_langs` - tłumaczenia produktów -- `pp_shop_products_images` - zdjęcia produktów -- `pp_shop_products_categories` - kategorie produktów -- `pp_shop_products_custom_fields` - pola własne produktów - -### Tabele integracji -- Kolumny w `pp_shop_products`: - - `apilo_product_id`, `apilo_product_name`, `apilo_get_data_date` -- Tabele ustawien: - - `pp_shop_apilo_settings` (key-value) - - `pp_shop_shoppro_settings` (key-value) - -## Konfiguracja - -### Redis -- Konfiguracja: `config.php` (zmienna `$config['redis']`) -- Parametry: host, port, password - -### Autoload -- Funkcja: `__autoload_my_classes()` w `cron.php:6` -- Wzorzec: `autoload/{namespace}/class.{ClassName}.php` - -## Klasy pomocnicze - -### \S (autoload/class.S.php) -Główna klasa helper z metodami: -- `seo($val)` - generowanie URL SEO -- `normalize_decimal($val, $precision)` - normalizacja liczb -- `send_email()` - wysyłanie emaili -- `delete_dir($dir)` - usuwanie katalogów -- `htacces()` - generowanie .htaccess i sitemap.xml - -### Medoo -- Plik: `libraries/medoo/medoo.php` -- Zmienna: `$mdb` -- ORM do operacji na bazie danych - -## Najważniejsze wzorce - -### Namespace'y -- `\admin\Controllers\` - nowe kontrolery panelu admin (DI) -- `\admin\controls\` - kontrolery legacy (fallback) -- `\Domain\` - repozytoria/logika domenowa -- `\admin\factory\` - helpery/fabryki admin -- `\front\factory\` - helpery/fabryki frontend -- `\shop\` - klasy sklepu (Product, Order, itp.) - -### Cachowanie produktów -```php -// Pobranie produktu z cache -$product = \shop\Product::getFromCache($product_id, $lang_id, $permutation_hash); - -// Czyszczenie cache produktu -\S::clear_product_cache($product_id); - -// Czyszczenie całego cache -\S::clear_redis_cache(); -``` - -## Refaktoryzacja do Domain-Driven Architecture - -### Nowa struktura (w trakcie migracji) -``` -autoload/ -├── Domain/ # Nowa warstwa biznesowa (namespace \Domain\) -│ ├── Product/ -│ │ └── ProductRepository.php # getQuantity, getPrice, getName, find, updateQuantity, archive, unarchive -│ ├── Banner/ -│ │ └── BannerRepository.php # find, delete, save -│ ├── Settings/ -│ │ └── SettingsRepository.php # saveSettings, getSettings (fasada → factory) -│ └── Cache/ -│ └── CacheRepository.php # clearCache (dirs + Redis) -├── admin/ -│ ├── Controllers/ # Nowe kontrolery (namespace \admin\Controllers\) -│ │ ├── BannerController.php # DI, instancyjny -│ │ ├── SettingsController.php # DI, instancyjny (clearCache, save, view) -│ │ ├── ProductArchiveController.php # DI, instancyjny (list, unarchive) -│ │ └── UsersController.php # DI, instancyjny (view_list, user_edit, user_save, user_delete, login_form, twofa) -│ ├── class.Site.php # Router: nowy kontroler → fallback stary -│ ├── controls/ # Stare kontrolery (niezależny fallback) -│ ├── factory/ # Stare helpery (niezależny fallback) -│ └── view/ # Widoki (statyczne - bez zmian) -├── shop/ # Legacy - fasady do Domain -└── front/factory/ # Legacy - stopniowo migrowane -``` - -#### Aktualny stan migracji (uzupełnienie) -- Dodane repozytorium: `Domain\Dictionaries\DictionariesRepository` -- Dodane kontrolery DI: `admin\Controllers\DictionariesController`, `admin\Controllers\FilemanagerController`, `admin\Controllers\UsersController` -- Dodane repozytorium: `Domain\User\UserRepository` -- `Domain\Settings\SettingsRepository` działa bezpośrednio na DB (bez delegacji do `admin\factory\Settings`) - -### Routing admin (admin\Site::route()) -1. Sprawdź mapę `$newControllers` → utwórz instancję z DI → wywołaj -2. Jeśli nowy kontroler nie istnieje (`class_exists()` = false) → fallback na `admin\controls\` -3. Stary kontroler jest NIEZALEŻNY od nowych klas (bezpieczny fallback) - -### Dependency Injection -Nowe klasy używają **Dependency Injection** zamiast `global` variables: -```php -// STARE -global $mdb; -$quantity = $mdb->get('pp_shop_products', 'quantity', ['id' => $id]); - -// NOWE -$repository = new \Domain\Product\ProductRepository($mdb); -$quantity = $repository->getQuantity($id); -``` - -## Testowanie (tylko dla deweloperów) - -**UWAGA:** Pliki testów NIE są częścią aktualizacji dla klientów! - -### Narzędzia -- **PHPUnit 9.6.34** - framework testowy -- **test.bat** - uruchamianie testów -- **composer.json** - autoloading PSR-4 - -### Struktura -``` -tests/ -├── Unit/ -│ ├── Domain/ -│ │ ├── Product/ProductRepositoryTest.php # 11 testów -│ │ ├── Banner/BannerRepositoryTest.php # 4 testy -│ │ ├── Settings/SettingsRepositoryTest.php # 3 testy -│ │ └── Cache/CacheRepositoryTest.php # 4 testy -│ └── admin/ -│ └── Controllers/ -│ ├── SettingsControllerTest.php # 7 testów -│ └── ProductArchiveControllerTest.php # 6 testów -└── Integration/ -``` -Aktualnie w suite są też testy modułów `Dictionaries`, `Articles` i `Users` (repozytoria + kontrolery DI). -**Łącznie: 119 tests, 256 assertions** - -## Ostatnie modyfikacje - -### 2026-02-10: Porządki po migracji i release 0.252 (ver. 0.252) -- **UPDATE:** `ProductArchiveController` i szablony listy archiwum przepięte na nową tabelę (`components/table-list`) -- **UPDATE:** CSS/JS dla list wydzielone do osobnych widoków `*-custom-script.php` (banery i archiwum produktów) -- **UPDATE:** dodano `admin\Controllers\FilemanagerController` i przepięto filemanager na nowy routing -- **FIX:** naprawiono błąd `Invalid Key` w filemanagerze -- **CLEANUP:** usunięto legacy pliki: `autoload/admin/controls/class.Archive.php`, `autoload/admin/controls/class.Filemanager.php`, `autoload/admin/view/class.FileManager.php`, stare szablony `admin/templates/product_archive/*` -- **RENAME:** folder szablonów `admin/templates/product_archive/` → `admin/templates/product-archive/` -- Testy: 82 tests, 181 assertions - -### 2026-02-09: Migracja Dictionaries (ver. 0.251) -- **NEW:** `Domain\Dictionaries\DictionariesRepository` (listForAdmin, find, save, delete, allUnits) -- **NEW:** `admin\Controllers\DictionariesController` (lista + formularz na nowych komponentach) -- **UPDATE:** migracja słowników na `components/table-list` i `components/form-edit` -- **FIX:** obsługa `lang_id` jako string (`pl`, `en`) w zapisie tłumaczeń -- **CLEANUP:** usunięto legacy klasy Dictionaries (`admin\controls`, `admin\factory`, `front\factory`) -- Testy: 82 tests, 181 assertions - -### 2026-02-09: Refaktoryzacja Settings (ver. 0.250) -- **UPDATE:** `Domain\Settings\SettingsRepository` ma bezpośredni dostęp do DB (bez delegacji do `admin\factory\Settings`) -- **UPDATE:** przepięto użycia `admin\factory\Settings` na `Domain\Settings\SettingsRepository` -- **CLEANUP:** usunięto legacy klasy Settings (`factory`, `controls`, `view`) -- Testy: 82 tests, 181 assertions - -### 2026-02-07: Usuniecie legacy kontrolera Articles (ver. 0.246) -- **UPDATE:** usunieto `autoload/admin/controls/class.Articles.php` -- **UPDATE:** `admin\Controllers\ArticlesController::galleryOrderSave()` uzywa `Domain\Article\ArticleRepository::saveGalleryOrder()` -- **UPDATE:** `Domain\Article\ArticleRepository` - dodano `saveGalleryOrder(int $articleId, string $order): bool` -- **UPDATE:** `admin\factory\Articles::gallery_order_save()` deleguje do `ArticleRepository::saveGalleryOrder()` (backward compatibility) -- **FIX:** sortowanie list admin po reloadzie - `RewriteRule` dla `/admin/...` ma `QSA` -- **FIX:** generator `\S::htacces()` komentuje dyrektywy `AddHandler|SetHandler|ForceType` (kompatybilnosc hostingu) -- **UPDATE:** zrodlo generatora `libraries/htaccess.conf` dostosowane do powyzszych zmian -- **WAZNE (deploy):** w paczce aktualizacji dodac `ver_X.XXX_files.txt` z wpisem: - `F: ../autoload/admin/controls/class.Articles.php` -- Testy: 65 tests, 131 assertions - -### 2026-02-06: Migracja Articles::article_delete do DI (ver. 0.245) -- **UPDATE:** `Domain\Article\ArticleRepository` - dodano `archive()` (ustawia status = -1) -- **UPDATE:** `admin\Controllers\ArticlesController` - nowa akcja `delete()` z DI -- **UPDATE:** Router `admin\Site` - dodano `'article_delete' => 'delete'` do `$actionMap` -- **UPDATE:** `admin\factory\Articles::articles_set_archive()` deleguje do `ArticleRepository::archive()` -- **UPDATE:** `admin\controls\Articles::article_delete()` oznaczone `@deprecated` -- Testy: 59 tests, 123 assertions - -### 2026-02-06: Migracja Articles::article_save do DI (ver. 0.244) -- **UPDATE:** `Domain\Article\ArticleRepository` - dodano `save()` + prywatne helpery (`buildArticleRow`, `buildLangRow`, `saveTranslations`, `savePages`, `assignTempFiles`, `assignTempImages`, `deleteMarkedFiles`, `deleteMarkedImages`, `maxPageOrder`) -- **UPDATE:** `admin\Controllers\ArticlesController` - nowa akcja `save()` z DI -- **UPDATE:** Router `admin\Site` - dodano `'article_save' => 'save'` do `$actionMap` -- **UPDATE:** `admin\factory\Articles::article_save()` deleguje do `ArticleRepository::save()` (backward compatibility) -- **UPDATE:** `admin\controls\Articles::article_save()` oznaczone `@deprecated` -- **UPDATE:** `tests/bootstrap.php` - dodano stub `S::seo()` -- Testy: 57 tests, 119 assertions - -### 2026-02-06: Articles cleanup moved to repository (ver. 0.243) -- **UPDATE:** `Domain\Article\ArticleRepository` - added `deleteNonassignedImages()` and `deleteNonassignedFiles()` -- **UPDATE:** `admin\Controllers\ArticlesController::edit()` uses repository cleanup methods -- **UPDATE:** `admin\factory\Articles::delete_nonassigned_images()` and `delete_nonassigned_files()` delegate to repository (backward compatibility) -- Testy: 50 tests, 95 assertions - -### 2026-02-06: Migracja Articles::article_edit do DI (ver. 0.242) -- **NOWE:** `Domain\Article\ArticleRepository` - repozytorium artykułów (`find()`) -- **UPDATE:** `admin\Controllers\ArticlesController` - konstruktor DI + `edit()` używa repozytorium -- **UPDATE:** Router `admin\Site` - factory dla `ArticlesController` z `ArticleRepository` -- **UPDATE:** `admin\factory\Articles::article_details()` deleguje do `Domain\Article\ArticleRepository` -- **UPDATE:** Stare kontrolery `admin\controls\Articles|Banners|Settings` - metody przejęte przez nowe kontrolery oznaczone `@deprecated` -- Testy: 48 testów, 91 asercji - -### 2026-02-06: Migracja ProductArchive (ver. 0.241) -- **NOWE:** `admin\Controllers\ProductArchiveController` - kontroler archiwum produktów z DI -- **NOWE:** `ProductRepository::archive()`, `unarchive()` - operacje archiwizacji w repozytorium -- **RENAME:** `admin/templates/archive/` → `admin/templates/product_archive/` -- **FIX:** SQL w `ajax_products_list_archive()` - puste wyszukiwanie generowało `name|ean|sku LIKE '%%'` (NULL bitwise OR filtrował wyniki) -- **FIX:** Brakujący `archive = 1` w branchu bez wyszukiwania -- **CLEANUP:** Usunięto zbędny JS z szablonu archiwum (apilo, baselinker, duplikowanie, edycja cen) -- Stary kontroler `admin\controls\Archive` zachowany jako fallback -- Testy: 50 tests, 95 assertions (+10 nowych) - -### 2026-02-05: Migracja Settings + Cache (ver. 0.240) -- **NOWE:** `Domain\Settings\SettingsRepository` - repozytorium ustawień (fasada → factory) -- **NOWE:** `Domain\Cache\CacheRepository` - repozytorium cache (dirs + Redis) -- **NOWE:** `admin\Controllers\SettingsController` - kontroler z DI (clearCache, save, view) -- **FIX:** Brakujący `id="content"` w main-layout.php (komunikaty grid.js) -- **FIX:** `persist_edit = true` w settings.php (komunikat po zapisie) -- Stary kontroler `admin\controls\Settings` zachowany jako fallback -- Testy: 29 testów, 60 asercji (+14 nowych) -- Bootstrap testów: stuby klas systemowych (S, RedisConnection, Redis, CacheHandler) - -### 2026-02-05: Migracja Banner + Product (ver. 0.239) -- **NOWE:** `Domain\Banner\BannerRepository` - repozytorium banerów (find, delete, save) -- **NOWE:** `admin\Controllers\BannerController` - pierwszy kontroler z DI -- **NOWE:** Router z mapą `$newControllers` + fallback na stare kontrolery -- **NOWE:** Autoloader PSR-4 fallback w 9 entry pointach -- Zmigrowano: `get_product_price()` → `ProductRepository::getPrice()` -- Zmigrowano: `get_product_name()` → `ProductRepository::getName()` -- Testy: 15 testów, 31 asercji - -### 2025-02-05: Refaktoryzacja - Product Repository (ver. 0.238) -- **NOWE:** `Domain\Product\ProductRepository` - pierwsza klasa w nowej architekturze -- **NOWE:** Dependency Injection zamiast `global $mdb` -- **NOWE:** Testy jednostkowe (5 testów, 100% pokrycie) -- Zmigrowano: `get_product_quantity()` → `ProductRepository::getQuantity()` -- Kompatybilność: Stara klasa `shop\Product` działa jako fasada - -### 2025-02-05: System cache produktów (ver. 0.237) -- Automatyczne czyszczenie cache produktu po aktualizacji przez CRON -- AJAX dla przycisku "Wyczyść cache" w panelu admin -- Metody `delete()` i `deletePattern()` w CacheHandler -- Metoda `clear_product_cache()` w klasie S - ---- -*Dokument aktualizowany: 2026-02-12* - - -### 2026-02-12: Migracja Users (/admin/users) (ver. 0.253) -- **NOWE:** `Domain\User\UserRepository` - repozytorium uzytkownikow (CRUD, check_login, logon, details, 2FA) -- **NOWE:** `admin\Controllers\UsersController` - kontroler DI dla akcji `view_list`, `user_edit`, `user_save`, `user_delete`, `login_form`, `twofa` -- **UPDATE:** `admin\Site` - dodany factory wpis dla modulu `Users` w mapie nowych kontrolerow -- **UPDATE:** `admin\factory\Users` - fasada deleguje logike do `Domain\User\UserRepository` -- **UPDATE:** `admin/ajax/users.php` - `check_login` korzysta bezposrednio z `UserRepository` -- **CLEANUP:** usuniety `autoload/admin/controls/class.Users.php` (brak fallback - nowy kontroler obsluguje wszystkie akcje) -- Testy: 119 tests, 256 assertions - ---- -*Dokument aktualizowany: 2026-02-12* -- **UPDATE:** widoki Users przeniesione z `grid/gridEdit` na `components/table-list` i `components/form-edit` - -## Aktualizacja 2026-02-12 (finalizacja Users) -- Modu� users dzia�a na `Domain\\User\\UserRepository` + `admin\\Controllers\\UsersController`. -- Usuni�to legacy klasy: `autoload/admin/controls/class.Users.php`, `autoload/admin/factory/class.Users.php`, `autoload/admin/view/class.Users.php`. -- Walidacja: przy w��czonym 2FA pole `twofa_email` jest wymagane. -- Widoki users przeniesione na `components/table-list` i `components/form-edit`. -- **NOWE:** `Domain\\Languages\\LanguagesRepository` - repozytorium jezykow i tlumaczen (lista, zapis, usuwanie, max_order) -- **NOWE:** `admin\\Controllers\\LanguagesController` - kontroler DI (`list/view_list`, `language_*`, `translation_*`) -- **UPDATE:** modul Languages przepiety z `grid/gridEdit` na `components/table-list` i `components/form-edit` -- **CLEANUP:** usuniete legacy klasy `autoload/admin/controls/class.Languages.php`, `autoload/admin/view/class.Languages.php` -- Testy: 130 tests, 301 assertions - -## Aktualizacja 2026-02-12 (Languages final) -- Dodano `Domain\\Languages\\LanguagesRepository` oraz `admin\\Controllers\\LanguagesController`. -- Modul `/admin/languages/` (jezyki + tlumaczenia) dziala na nowym routingu DI. -- Widoki jezykow przepiete na `components/table-list` i `components/form-edit`. -- Usunieto legacy: `autoload/admin/controls/class.Languages.php`, `autoload/admin/view/class.Languages.php`. - -## Aktualizacja 2026-02-12 (ver. 0.255) -- UPDATE: admin/Controllers/SettingsController, BannerController, DictionariesController, ArticlesController pobieraja listy jezykow przez Domain/Languages/LanguagesRepository (DI), bez zaleznosci od admin/factory/Languages. -- UPDATE: w admin/Site fabryki DI dla Articles, Banners, Settings, Dictionaries przekazuja rowniez LanguagesRepository. -- UPDATE: legacy admin/controls/* oraz admin/factory/Shop* przepiete z admin/factory/Languages::languages_list() na bezposrednie wywolania LanguagesRepository. -- FIX: autoload/admin/factory/class.Languages.php uzywa pelnego znacznika `edit/save/delete`. -- UPDATE: `admin\factory\Scontainers` dziala jako fasada do `Domain\Scontainers\ScontainersRepository`. -- UPDATE: `front\factory\Scontainers` korzysta z `Domain\Scontainers\ScontainersRepository`. -- CLEANUP: usuniete legacy klasy `autoload/admin/controls/class.Scontainers.php`, `autoload/admin/view/class.Scontainers.php`. -- Testy: 158 tests, 397 assertions. - -## Aktualizacja 2026-02-12 (ver. 0.260) -- NOWE: `Domain\Article\ArticleRepository` rozszerzone o `listArchivedForAdmin()`, `restore()`, `deletePermanently()`. -- NOWE: `admin\Controllers\ArticlesArchiveController` (DI) dla akcji `list/view_list`, `article_restore`, `article_delete`. -- UPDATE: routing DI (`admin\Site`) rozszerzony o modul `ArticlesArchive` oraz mapowanie akcji `article_restore -> restore`. -- UPDATE: `/admin/articles_archive/view_list/` przepiete z legacy `grid` na `components/table-list`. -- CLEANUP: usuniete legacy klasy `autoload/admin/controls/class.ArticlesArchive.php`, `autoload/admin/factory/class.ArticlesArchive.php`, `autoload/admin/view/class.ArticlesArchive.php`. -- Testy: 165 tests, 424 assertions. - -### 2026-02-13: Refaktoryzacja /admin/articles (ver. 0.261) -- **UPDATE:** routing DI dla `ArticlesController` obsluguje akcje AJAX: `article_image_alt_change`, `article_file_name_change`, `article_image_delete`, `article_file_delete`. -- **UPDATE:** widok `admin/templates/articles/article-edit.php` korzysta z endpointow `/admin/articles/*` zamiast `admin/ajax.php?a=article_*`. -- **UPDATE:** lista artykulow nie korzysta juz z `admin\factory\Articles::article_pages` (etykiety stron z `Domain\Article\ArticleRepository`). -- **CLEANUP:** usuniete legacy pliki `autoload/admin/view/class.Articles.php` i `admin/ajax/articles.php`; odpiecie include w `admin/ajax.php`. -- Testy: 176 tests, 439 assertions. - -### 2026-02-13: Articles edit UX i sortowanie zalacznikow (ver. 0.261) -- **UPDATE:** `Domain\Article\ArticleRepository` - dodane `saveFilesOrder()` oraz obsluga `files_order` podczas `save()` (pierwszy zapis zachowuje kolejnosc). -- **UPDATE:** routing DI (`admin\Site::$actionMap`) rozszerzony o `files_order_save -> filesOrderSave`. -- **UPDATE:** `admin\Controllers\ArticlesController` - nowa akcja AJAX `filesOrderSave()`. -- **UPDATE:** `admin/templates/articles/article-edit-custom-script.php` - drag&drop sortowania listy zalacznikow + synchronizacja hidden input `files_order`. -- **UPDATE:** potwierdzenia usuwania zdjec i zalacznikow w edycji artykulu ujednolicone wizualnie z dialogiem usuwania z listy (jquery-confirm, `table-list-confirm-dialog`). -- **FIX:** dodane ladowanie biblioteki `jquery-impromptu` w widoku edycji artykulu (kompatybilnosc dla `$.prompt`). -- Testy: 178 tests, 443 assertions. - -## Aktualizacja 2026-02-13 (Pages) -- NOWE: `Domain\\Pages\\PagesRepository` (menu/page CRUD, drzewo stron, sortowanie, SEO, endpointy pomocnicze). -- NOWE: `admin\\Controllers\\PagesController` (DI) dla modulu `/admin/pages/*`. -- UPDATE: widoki `admin/templates/pages/*` dzialaja bez `admin\\factory\\Pages` i `admin\\view\\Pages`. -- UPDATE: routing DI (`admin\\Site`) ma fabryke kontrolera `Pages`. -- UPDATE: zalezne endpointy `cookie_*` i `generate_seo_link` przepiete na `/admin/pages/*`. -- CLEANUP: usuniete legacy pliki `autoload/admin/controls/class.Pages.php`, `autoload/admin/view/class.Pages.php`, `autoload/admin/factory/class.Pages.php`, `admin/ajax/pages.php`. - -## Aktualizacja 2026-02-13 (Integrations refactor, ver. 0.263) -- NOWE: `Domain\Integrations\IntegrationsRepository` (settings Apilo/ShopPRO, OAuth, product linking, API fetch). -- NOWE: `admin\Controllers\IntegrationsController` (DI) dla akcji Apilo (settings, authorization, fetch lists, product CRUD) i ShopPRO (settings, product import). -- UPDATE: `admin\factory\Integrations` jako fasada delegujaca do repozytorium (tylko Apilo + ShopPRO). -- CLEANUP: **usunieto integracje Sellasist i Baselinker z calego projektu** - kontrolery, factory, szablony, referencje w cron.php, Order, ShopStatuses, ShopTransport, ShopPaymentMethod, ShopProduct, config.php, front/factory/*. -- CLEANUP: usuniete pliki: `autoload/admin/controls/class.Integrations.php`, `autoload/admin/controls/class.Baselinker.php`, `autoload/admin/factory/class.Baselinker.php`, `autoload/front/factory/class.Shop.php`, `autoload/shop/class.ShopStatus.php`, szablony sellasist/baselinker. -- Testy: **OK (212 tests, 577 assertions)**. - -## Aktualizacja 2026-02-13 (ShopPromotion refactor, ver. 0.264) -- NOWE: `Domain\Promotion\PromotionRepository` (listForAdmin, find, save, delete, categoriesTree + invalidacja cache aktywnych promocji). -- NOWE: `admin\Controllers\ShopPromotionController` (DI) dla akcji `list`, `edit`, `save`, `delete`. -- UPDATE: modul `/admin/shop_promotion/*` przepiety z legacy `grid/gridEdit` na `components/table-list` i `components/form-edit`. -- NOWE: widoki `admin/templates/shop-promotion/promotions-list.php`, `admin/templates/shop-promotion/promotion-edit.php`. -- NOWE: partiale drzewa kategorii: `admin/templates/shop-promotion/promotion-categories-selector.php`, `admin/templates/shop-promotion/promotion-categories-tree.php`. -- NOWE: `admin/templates/shop-promotion/promotion-edit-custom-script.php` (logika warunkow promocji + drzewo kategorii). -- CLEANUP: usuniete legacy klasy/pliki `autoload/admin/controls/class.ShopPromotion.php`, `autoload/admin/factory/class.ShopPromotion.php`, `admin/templates/shop-promotion/view-list.php`. -- UPDATE: menu admin wskazuje kanoniczny URL `/admin/shop_promotion/list/`. -- Testy: **OK (222 tests, 609 assertions)**. - -## Aktualizacja 2026-02-13 (ShopPromotion poprawki, ver. 0.265) -- UPDATE: dodano pole `Data od` (`date_from`) w module `/admin/shop_promotion` (repozytorium, formularz i lista). -- UPDATE: `shop\Promotion::get_active_promotions()` uwzglednia `date_from` (`NULL` lub `<= dzisiaj`) obok `date_to`. -- FIX: edycja promocji zapisuje update zamiast insert (stabilne przekazanie `id` przez hidden field + fallback `id` z URL w `save()`). -- Testy: **OK (222 tests, 614 assertions)**. - -## Aktualizacja 2026-02-13 (ShopCoupon refactor, ver. 0.266) -- NOWE: `Domain\Coupon\CouponRepository` (`listForAdmin`, `find`, `save`, `delete`, `categoriesTree`). -- NOWE: `admin\Controllers\ShopCouponController` (DI) dla akcji `list`, `edit`, `save`, `delete`. -- UPDATE: zachowana kompatybilnosc aliasow legacy akcji (`view_list`, `coupon_edit`, `coupon_save`, `coupon_delete`) w nowym kontrolerze. -- UPDATE: modul `/admin/shop_coupon/*` przepiety z legacy `grid/gridEdit` na `components/table-list` i `components/form-edit`. -- NOWE: widoki/partiale `shop-coupon/coupons-list`, `shop-coupon/coupon-edit-new`, `shop-coupon/coupon-categories-selector`, `shop-coupon/coupon-categories-tree`, `shop-coupon/coupon-edit-custom-script`. -- CLEANUP: usuniete legacy klasy/pliki `autoload/admin/controls/class.ShopCoupon.php`, `autoload/admin/factory/class.ShopCoupon.php`, `admin/templates/shop-coupon/view-list.php`, `admin/templates/shop-coupon/coupon-edit.php`. -- UPDATE: menu admin wskazuje kanoniczny URL `/admin/shop_coupon/list/`. -- FIX: ujednolicone zachowanie drzewek i styl checkboxow miedzy widokami `/admin/shop_coupon/edit/*` i `/admin/layouts/edit/*` (strzalki, focus, iCheck). -- Testy: **OK (235 tests, 682 assertions)**. diff --git a/REFACTORING_PLAN.md b/REFACTORING_PLAN.md deleted file mode 100644 index 7aa43de..0000000 --- a/REFACTORING_PLAN.md +++ /dev/null @@ -1,873 +0,0 @@ -# Plan Refaktoryzacji shopPRO - Domain-Driven Architecture - -## Cel -Stopniowe przeniesienie logiki biznesowej do architektury warstwowej: -- **Domain/** - logika biznesowa (core) -- **Admin/** - warstwa administratora -- **Frontend/** - warstwa użytkownika -- **Shared/** - współdzielone narzędzia - -## Docelowa struktura - -``` -autoload/ -├── Domain/ # Logika biznesowa (CORE) - namespace \Domain\ -│ ├── Product/ -│ │ ├── ProductRepository.php # ✅ Zmigrowane (getQuantity, getPrice, getName, find, updateQuantity, archive, unarchive) -│ │ ├── ProductService.php # Logika biznesowa (przyszłość) -│ │ └── ProductCacheService.php # Cache produktu (przyszłość) -│ ├── Banner/ -│ │ └── BannerRepository.php # ✅ Zmigrowane (find, delete, save) -│ ├── Settings/ -│ │ └── SettingsRepository.php # ✅ Zmigrowane (saveSettings, getSettings) - bezposrednio DB -│ ├── Dictionaries/ -│ │ └── DictionariesRepository.php # ✅ Zmigrowane (listForAdmin, find, save, delete, allUnits) -│ ├── Cache/ -│ │ └── CacheRepository.php # ✅ Zmigrowane (clearCache) -│ ├── Order/ -│ ├── Category/ -│ └── ... -│ -├── admin/ # Warstwa administratora (istniejący katalog!) -│ ├── Controllers/ # Nowe kontrolery - namespace \admin\Controllers\ -│ │ ├── ArticlesController.php -│ │ ├── BannerController.php -│ │ ├── DictionariesController.php -│ │ ├── FilemanagerController.php -│ │ ├── ProductArchiveController.php -│ │ └── SettingsController.php -│ ├── controls/ # Stare kontrolery (legacy fallback) -│ ├── factory/ # Stare helpery (legacy) -│ └── view/ # Widoki (statyczne - OK bez zmian) -│ -├── Frontend/ # Warstwa użytkownika (przyszłość) -│ ├── Controllers/ -│ └── Services/ -│ -├── Shared/ # Współdzielone narzędzia -│ ├── Cache/ -│ │ ├── CacheHandler.php -│ │ └── RedisConnection.php -│ └── Helpers/ -│ └── S.php -│ -└── [LEGACY] # Stare klasy (stopniowo deprecated) - ├── shop/ - ├── admin/factory/ - └── front/factory/ -``` - -### WAŻNE: Konwencja namespace → katalog (Linux case-sensitive!) -- `\Domain\` → `autoload/Domain/` (duże D - nowy katalog) -- `\admin\Controllers\` → `autoload/admin/Controllers/` (małe a - istniejący katalog) -- NIE używać `\Admin\` (duże A) bo na serwerze Linux katalog to `admin/` (małe a) - -## Zasady migracji - -### 1. Stopniowość -- Przenosimy **jedną funkcję na raz** -- Zachowujemy kompatybilność wsteczną -- Stare klasy działają jako fasady do nowych - -### 2. Dependency Injection zamiast statycznych metod -```php -// ❌ STARE - statyczne -class Product { - public static function getQuantity($id) { - global $mdb; - return $mdb->get('pp_shop_products', 'quantity', ['id' => $id]); - } -} - -// ✅ NOWE - instancje z DI -class ProductRepository { - private $db; - - public function __construct($db) { - $this->db = $db; - } - - public function getQuantity($id) { - return $this->db->get('pp_shop_products', 'quantity', ['id' => $id]); - } -} -``` - -### 3. Fasady dla kompatybilności -```php -// Stara klasa wywołuje nową -namespace shop; - -class Product { - public static function getQuantity($id) { - global $mdb; - $repo = new \Domain\Product\ProductRepository($mdb); - return $repo->getQuantity($id); - } -} -``` - -## Proces migracji funkcji - -### Krok 1: Wybór funkcji -- Wybierz prostą funkcję statyczną -- Sprawdź jej zależności -- Przeanalizuj gdzie jest używana - -### Krok 2: Stworzenie nowej struktury -- Utwórz folder `Domain/{Module}/` -- Stwórz odpowiednią klasę (Repository/Service/Entity) -- Przenieś logikę - -### Krok 3: Znalezienie użyć -```bash -grep -r "Product::getQuantity" . -``` - -### Krok 4: Aktualizacja wywołań -- Opcja A: Bezpośrednie wywołanie nowej klasy -- Opcja B: Fasada w starej klasie (zalecane na początek) - -### Krok 5: Testy -- Napisz test jednostkowy dla nowej funkcji -- Sprawdź czy stare wywołania działają - -## Status migracji - -### ✅ Zmigrowane moduły -- **Cache** (częściowo) - - ✅ CacheHandler - ma delete/deletePattern - - ✅ RedisConnection - singleton - - ✅ S::clear_product_cache() - nowa metoda - -### 🔄 Status modułów -- **Product** - - ✅ get_product_quantity() - **ZMIGROWANE** (2025-02-05) 🎉 - - Nowa klasa: `Domain\Product\ProductRepository::getQuantity()` - - Fasada w: `shop\Product::get_product_quantity()` - - Test: `tests/Unit/Domain/Product/ProductRepositoryTest.php` - - Testy: ✅ 5/5 przechodzą - - Aktualizacja: ver. 0.238 - - Użycie DI: ✅ Konstruktor przyjmuje `$db` - - ✅ get_product_price() - **ZMIGROWANE** (2026-02-05) 🎉 - - Nowa metoda: `Domain\Product\ProductRepository::getPrice()` - - Fasada w: `shop\Product::get_product_price()` - - Testy: ✅ 4 nowe testy (cena regularna, promocyjna, promo wyższa, nie znaleziono) - - Użycie: `front\factory\ShopPromotion` (linia 132) - - Aktualizacja: ver. 0.239 - - ✅ get_product_name() - **ZMIGROWANE** (2026-02-05) 🎉 - - Nowa metoda: `Domain\Product\ProductRepository::getName()` - - Fasada w: `shop\Product::get_product_name()` - - Testy: ✅ 2 nowe testy (nazwa znaleziona, nie znaleziona) - - Użycie: brak aktywnych wywołań (przygotowane na przyszłość) - - Aktualizacja: ver. 0.239 - - ✅ archive() / unarchive() - **ZMIGROWANE** (2026-02-06) 🎉 - - Nowe metody: `Domain\Product\ProductRepository::archive()`, `unarchive()` - - Nowy kontroler: `admin\Controllers\ProductArchiveController` (DI, instancyjny) - - Szablony: `admin/templates/product-archive/` (rename z `archive/`, dawniej `product_archive/`) - - Testy: ✅ 4 nowe testy repozytorium + 6 testów kontrolera - - FIX: SQL bug w `ajax_products_list_archive()` (puste wyszukiwanie + brak `archive = 1`) - - Dalsze porządki: ver. 0.252 (nowy table-list, wydzielony custom script, usunięte legacy pliki) - - Aktualizacja: ver. 0.241 / 0.252 - - [ ] is_product_on_promotion() - NASTĘPNA 👉 - -- **Banner** (DEMO pełnej migracji kontrolera) - - ✅ BannerRepository - **ZMIGROWANE** (2026-02-05) 🎉 - - Nowa klasa: `Domain\Banner\BannerRepository` (find, delete, save, saveTranslations) - - Nowy kontroler: `admin\Controllers\BannerController` (DI, instancyjny) - - Router: `admin\Site::route()` → sprawdza nowy kontroler → fallback na stary - - Testy: ✅ 4 testy (find z tłumaczeniami, not found, delete, save) - - Stary kontroler `admin\controls\Banners` działa jako niezależny fallback - - Stara factory `admin\factory\Banners` zachowana bez zmian (fallback) - - Aktualizacja: ver. 0.239 - -- **Articles** (migracja kontrolera - etap edit/details) - - ✅ ArticleRepository::find() - **ZMIGROWANE** (2026-02-06) 🎉 - - Nowa klasa: `Domain\Article\ArticleRepository` (find: artykul + relacje) - - Nowy kontroler: `admin\Controllers\ArticlesController` (DI, instancyjny) - - Zmigrowana akcja: `article_edit` -> `edit` (mapowanie w `admin\Site::$actionMap`) - - Kompatybilność: `admin\factory\Articles::article_details()` deleguje do nowego repozytorium - - Legacy cleanup: metody przejęte przez nowe kontrolery oznaczone `@deprecated` w `admin\controls\Articles|Banners|Settings` - - Testy repozytorium rozszerzone o czyszczenie nieprzypisanych plik�w/zdj�� - - Aktualizacja: ver. 0.243 - - ✅ ArticleRepository::save() - **ZMIGROWANE** (2026-02-06) 🎉 - - Metoda `save()` z prywatnych helperow (buildArticleRow, buildLangRow, saveTranslations, savePages, assignTempFiles, assignTempImages, deleteMarkedFiles, deleteMarkedImages) - - Zmigrowana akcja: `article_save` -> `save` (mapowanie w `admin\Site::$actionMap`) - - Kompatybilnosc: `admin\factory\Articles::article_save()` deleguje do repozytorium - - Testy: 7 nowych testow save (create, update, translations, pages, marked delete) - - Aktualizacja: ver. 0.244 - - ✅ ArticleRepository::archive() - **ZMIGROWANE** (2026-02-06) 🎉 - - Metoda `archive()` (ustawia status = -1) - - Zmigrowana akcja: `article_delete` -> `delete` (mapowanie w `admin\Site::$actionMap`) - - Kompatybilnosc: `admin\factory\Articles::articles_set_archive()` deleguje do repozytorium - - Testy: 2 nowe testy archive (success, failure) - - Aktualizacja: ver. 0.245 - - ✅ ArticlesController::browseList() - **ZMIGROWANE** (2026-02-07) 🎉 - - Nowa metoda kontrolera: `browseList()` (DI, instancyjna) - - Zmigrowana akcja: `browse_list` -> `browseList` (mapowanie w `admin\Site::$actionMap`) - - Legacy cleanup: usuniety `autoload/admin/controls/class.Articles.php` (brak fallback dla modułu Articles) - - Testy: 2 nowe testy kontraktu kontrolera (method exists + return type) - - ✅ ArticlesController::galleryOrderSave() - **ZMIGROWANE** (2026-02-07) 🎉 - - Nowa metoda kontrolera: `galleryOrderSave()` (AJAX) - - Zmigrowana akcja: `gallery_order_save` -> `galleryOrderSave` (mapowanie w `admin\Site::$actionMap`) - - Implementacja: używa `Domain\Article\ArticleRepository::saveGalleryOrder()` - - Testy: 2 nowe testy kontraktu kontrolera (method exists + return type) - - ✅ Usuniecie legacy kontrolera Articles - **ZMIGROWANE** (2026-02-07) 🎉 - - Usuniety plik: `autoload/admin/controls/class.Articles.php` - - Wymaganie dla aktualizacji: dodac wpis do `ver_X.XXX_files.txt` - - Wpis do usuniecia: `F: ../autoload/admin/controls/class.Articles.php` - - ✅ Stabilizacja generatora `.htaccess` - **ZMIGROWANE** (2026-02-07) 🎉 - - FIX: regula admin ma `QSA` (query string dla sortowania/filtrow) - - FIX: `\S::htacces()` komentuje `AddHandler|SetHandler|ForceType` dla zgodnosci z hostingiem - - UPDATE: `libraries/htaccess.conf` zaktualizowany, aby poprawki nie znikaly po regeneracji - -- **Settings** (migracja kontrolera) - - ✅ SettingsRepository - **ZMIGROWANE** (2026-02-05) 🎉 - - Nowa klasa: `Domain\Settings\SettingsRepository` (saveSettings, getSettings) - - Aktualny stan: bezpośredni dostęp do DB (usunięta delegacja do `admin\factory\Settings`) - ver. 0.250 - - Nowy kontroler: `admin\Controllers\SettingsController` (DI, instancyjny) - - Testy: ✅ 3 testy (instancja, metody) - - Stary kontroler `admin\controls\Settings` zachowany jako fallback - - Aktualizacja: ver. 0.240 / 0.250 - - ✅ CacheRepository - **ZMIGROWANE** (2026-02-05) 🎉 - - Nowa klasa: `Domain\Cache\CacheRepository` (clearCache) - - Używa `\S::delete_dir()` + `\RedisConnection` - - Testy: ✅ 4 testy (z Redis, bez Redis, niedostępny, struktura) - - Aktualizacja: ver. 0.240 - -- **Dictionaries** (migracja kontrolera i repozytorium) - - ✅ DictionariesRepository - **ZMIGROWANE** (2026-02-09) 🎉 - - Nowa klasa: `Domain\Dictionaries\DictionariesRepository` (listForAdmin, find, save, delete, allUnits) - - Nowy kontroler: `admin\Controllers\DictionariesController` (DI, instancyjny) - - Migracja na nowe komponenty: `components/table-list` + `components/form-edit` - - Legacy cleanup: usunięto klasy z `admin\controls`, `admin\factory`, `front\factory` - - Aktualizacja: ver. 0.251 - -- **Filemanager** (migracja routingu) - - ✅ FilemanagerController - **ZMIGROWANE** (2026-02-10) 🎉 - - Nowy kontroler: `admin\Controllers\FilemanagerController` - - Naprawa błędu: `Invalid Key` - - Legacy cleanup: usunięto `autoload/admin/controls/class.Filemanager.php` i `autoload/admin/view/class.FileManager.php` - - Aktualizacja: ver. 0.252 - -- **Users** (migracja kontrolera i repozytorium) - - ✅ UserRepository - **ZMIGROWANE** (2026-02-12) 🎉 - - Nowa klasa: `Domain\User\UserRepository` (find, getById, save, delete, checkLogin, logon, details, updateById, sendTwofaCode, verifyTwofaCode) - - Nowy kontroler: `admin\Controllers\UsersController` (DI, instancyjny: view_list, user_edit, user_save, user_delete, login_form, twofa) - - Router: `admin\Site` - factory wpis dla modulu `Users` - - Fasada: `admin\factory\Users` deleguje do repozytorium (backward compatibility dla login/2FA flow) - - AJAX: `admin/ajax/users.php` - `check_login` oparty o `UserRepository` - - Legacy cleanup: usuniety `autoload/admin/controls/class.Users.php` - - Testy: 25 testow repozytorium (CRUD, logon, 2FA, checkLogin) + 12 testow kontrolera (kontrakty + normalizeUser) - -- **Integrations** (migracja kontrolera i repozytorium + cleanup Sellasist/Baselinker) - - ✅ IntegrationsRepository - **ZMIGROWANE** (2026-02-13) 🎉 - - Nowa klasa: `Domain\Integrations\IntegrationsRepository` (settings Apilo/ShopPRO, OAuth, product linking, API fetch) - - Nowy kontroler: `admin\Controllers\IntegrationsController` (DI, instancyjny) - - Router: `admin\Site` - factory wpis dla modulu `Integrations` - - Fasada: `admin\factory\Integrations` deleguje do repozytorium (tylko Apilo + ShopPRO) - - **CLEANUP:** usunieto integracje Sellasist i Baselinker z calego projektu - - Usuniete pliki: `controls/Integrations`, `controls/Baselinker`, `factory/Baselinker`, `front/factory/Shop`, `shop/ShopStatus`, szablony sellasist/baselinker - - Wyczyszczone referencje w: cron.php, Order, ShopStatuses, ShopTransport, ShopPaymentMethod, ShopProduct, config.php, front/factory/* - - Testy: 16 nowych testow (repozytorium) + 10 testow kontrolera - - Aktualizacja: ver. 0.263 - -### 📋 Do zrobienia -- Order -- Category -- ShopAttribute -- ShopProduct (factory) - -## Testowanie - -### Framework: PHPUnit -Instalacja: -```bash -composer require --dev phpunit/phpunit -``` - -### Struktura testów -``` -tests/ -├── Unit/ -│ ├── Domain/ -│ │ ├── Article/ArticleRepositoryTest.php -│ │ ├── Banner/BannerRepositoryTest.php -│ │ ├── Cache/CacheRepositoryTest.php -│ │ ├── Dictionaries/DictionariesRepositoryTest.php -│ │ ├── Product/ProductRepositoryTest.php -│ │ ├── Settings/SettingsRepositoryTest.php -│ │ ├── User/UserRepositoryTest.php -│ │ └── Integrations/IntegrationsRepositoryTest.php -│ └── admin/ -│ └── Controllers/ -│ ├── ArticlesControllerTest.php -│ ├── DictionariesControllerTest.php -│ ├── IntegrationsControllerTest.php -│ ├── ProductArchiveControllerTest.php -│ ├── SettingsControllerTest.php -│ └── UsersControllerTest.php -└── Integration/ -``` -**Łącznie: 212 testów, 577 asercji** - -### Przykład testu -```php -// tests/Unit/Domain/Product/ProductRepositoryTest.php -use PHPUnit\Framework\TestCase; -use Domain\Product\ProductRepository; - -class ProductRepositoryTest extends TestCase { - public function testGetQuantityReturnsCorrectValue() { - // Arrange - $mockDb = $this->createMock(\medoo::class); - $mockDb->method('get')->willReturn(10); - - $repo = new ProductRepository($mockDb); - - // Act - $quantity = $repo->getQuantity(123); - - // Assert - $this->assertEquals(10, $quantity); - } -} -``` - -## Zasady kodu - -### 1. SOLID Principles -- **S**ingle Responsibility - jedna klasa = jedna odpowiedzialność -- **O**pen/Closed - otwarty na rozszerzenia, zamknięty na modyfikacje -- **L**iskov Substitution - podklasy mogą zastąpić nadklasy -- **I**nterface Segregation - wiele małych interfejsów -- **D**ependency Inversion - zależności od abstrakcji - -### 2. Nazewnictwo -- **Entity** - `Product.php` (reprezentuje obiekt domenowy) -- **Repository** - `ProductRepository.php` (dostęp do danych) -- **Service** - `ProductService.php` (logika biznesowa) -- **Controller** - `ProductController.php` (obsługa requestów) - -### 3. Type Hinting -```php -// ✅ DOBRE -public function getQuantity(int $id): ?int { - return $this->db->get('pp_shop_products', 'quantity', ['id' => $id]); -} - -// ❌ ZŁE -public function getQuantity($id) { - return $this->db->get('pp_shop_products', 'quantity', ['id' => $id]); -} -``` - -## Narzędzia pomocnicze - -### Autoloader (produkcja) -Autoloader w 9 entry pointach obsługuje dwie konwencje: -1. `autoload/{namespace}/class.{ClassName}.php` (legacy) -2. `autoload/{namespace}/{ClassName}.php` (PSR-4, fallback) - -Entry pointy: `index.php`, `ajax.php`, `api.php`, `cron.php`, `cron-turstmate.php`, `download.php`, `admin/index.php`, `admin/ajax.php`, `cron/cron-xml.php` - -### Static Analysis -```bash -composer require --dev phpstan/phpstan -vendor/bin/phpstan analyse autoload/Domain -``` - -## Kolejność refaktoryzacji (priorytet) - -1. **Cache** ✅ -2. **Product** (w trakcie) - - ✅ getQuantity (ver. 0.238) - - ✅ getPrice (ver. 0.239) - - ✅ getName (ver. 0.239) - - ✅ archive / unarchive (ver. 0.241) - - [ ] is_product_on_promotion - NASTĘPNA 👉 - - [ ] getFromCache - - [ ] getProductImg -3. **Banner** ✅ (pełna migracja kontrolera, ver. 0.239) -4. **Settings** ✅ (pełna migracja repo/kontrolera + cleanup legacy, ver. 0.250) -5. **Dictionaries** ✅ (repo + kontroler + form/table, ver. 0.251) -6. **ProductArchive** ✅ (migracja kontrolera + cleanup szablonów, ver. 0.252) -7. **Filemanager** ✅ (migracja routingu + fix `Invalid Key`, ver. 0.252) -8. **Users** ✅ (repo + kontroler + 2FA + legacy cleanup, ver. 0.253) -9. **Pages** ✅ (repo + kontroler + drzewo stron + AJAX endpoints, ver. 0.262) -10. **Integrations** ✅ (repo + kontroler + cleanup Sellasist/Baselinker, ver. 0.263) -11. **Order** -12. **Category** -13. **ShopAttribute** - -- **Form Edit System** - Nowy uniwersalny system formularzy edycji - - ✅ Klasy ViewModel: `FormFieldType`, `FormField`, `FormTab`, `FormAction`, `FormEditViewModel` - - ✅ Walidacja: `FormValidator` z obsługą reguł per pole i sekcje językowe - - ✅ Persist: `FormRequestHandler` - zapamiętywanie danych przy błędzie walidacji - - ✅ Renderer: `FormFieldRenderer` - renderowanie wszystkich typów pól - - ✅ Szablon: `admin/templates/components/form-edit.php` - uniwersalny layout - - ✅ BannerController - przerobiony na nowy system formularzy - - Wspierane typy pól: text, number, email, password, date, datetime, switch, select, textarea, editor, image, file, hidden, lang_section - - Obsługa zakładek (vertical) i sekcji językowych (horizontal) - - **Do zrobienia**: Przerobić pozostałe kontrolery/formularze (Product, Category, Pages, itd.) - ---- -*Rozpoczęto: 2025-02-05* -*Ostatnia aktualizacja: 2026-02-12* - - -## Form Edit System - Dokumentacja użycia - -### Architektura - -``` -┌─────────────────────────────────────────────────────────────┐ -│ Controller │ -│ ┌─────────────────┐ ┌─────────────────┐ │ -│ │ edit() │ │ save() │ │ -│ │ - buduje VM │ │ - walidacja │ │ -│ │ - renderuje │ │ - zapis │ │ -│ └────────┬────────┘ └─────────────────┘ │ -└───────────┼─────────────────────────────────────────────────┘ - │ - ▼ -┌─────────────────────────────────────────────────────────────┐ -│ FormEditViewModel │ -│ - title, formId, data, fields, tabs, actions │ -│ - validationErrors, persist, languages │ -└───────────┬─────────────────────────────────────────────────┘ - │ - ▼ -┌─────────────────────────────────────────────────────────────┐ -│ components/form-edit.php (szablon) │ -│ ┌─────────────────────────────────────────────────────┐ │ -│ │ FormFieldRenderer - renderuje każde pole │ │ -│ │ ├─ input, select, textarea, switch │ │ -│ │ ├─ date, datetime, editor, image │ │ -│ │ └─ lang_section (zagnieżdżone pola) │ │ -│ └─────────────────────────────────────────────────────┘ │ -└─────────────────────────────────────────────────────────────┘ -``` - -### Przykład użycia w kontrolerze - -```php -use admin\ViewModels\Forms\FormEditViewModel; -use admin\ViewModels\Forms\FormField; -use admin\ViewModels\Forms\FormTab; -use admin\ViewModels\Forms\FormAction; -use admin\Support\Forms\FormRequestHandler; - -class BannerController -{ - public function edit(): string - { - $banner = $this->repository->find($id); - $languages = \admin\factory\Languages::languages_list(); - - $viewModel = new FormEditViewModel( - formId: 'banner-edit', - title: 'Edycja banera', - data: $banner, - tabs: [ - new FormTab('settings', 'Ustawienia', 'fa-wrench'), - new FormTab('content', 'Zawartość', 'fa-file'), - ], - fields: [ - // Zakładka Ustawienia - FormField::text('name', [ - 'label' => 'Nazwa', - 'tab' => 'settings', - 'required' => true, - ]), - FormField::switch('status', [ - 'label' => 'Aktywny', - 'tab' => 'settings', - ]), - FormField::date('date_start', [ - 'label' => 'Data rozpoczęcia', - 'tab' => 'settings', - ]), - - // Sekcja językowa w zakładce Zawartość - FormField::langSection('translations', 'content', [ - FormField::image('src', ['label' => 'Obraz']), - FormField::text('url', ['label' => 'Url']), - FormField::editor('text', ['label' => 'Treść']), - ]), - ], - actions: [ - FormAction::save('/admin/banners/save', '/admin/banners'), - FormAction::cancel('/admin/banners'), - ], - languages: $languages, - persist: true, - ); - - return \Tpl::view('components/form-edit', ['form' => $viewModel]); - } - - public function save(): void - { - $formHandler = new FormRequestHandler(); - $viewModel = $this->buildFormViewModel(); // jak w edit() - - $result = $formHandler->handleSubmit($viewModel, $_POST); - - if (!$result['success']) { - // Błędy walidacji - zapisane automatycznie do sesji - echo json_encode(['success' => false, 'errors' => $result['errors']]); - exit; - } - - // Sukces - persist wyczyszczony automatycznie - $this->repository->save($result['data']); - echo json_encode(['success' => true]); - exit; - } -} -``` - -### Dostępne typy pól - -| Typ | Metoda | Opcje | -|-----|--------|-------| -| `text` | `FormField::text(name, ['label' => '...', 'required' => true])` | placeholder, help | -| `number` | `FormField::number(name, [...])` | - | -| `email` | `FormField::email(name, [...])` | walidacja formatu | -| `password` | `FormField::password(name, [...])` | - | -| `date` | `FormField::date(name, [...])` | datetimepicker | -| `datetime` | `FormField::datetime(name, [...])` | datetimepicker z czasem | -| `switch` | `FormField::switch(name, [...])` | checked (bool) | -| `select` | `FormField::select(name, ['options' => [...]])` | options: [key => label] | -| `textarea` | `FormField::textarea(name, ['rows' => 4])` | rows | -| `editor` | `FormField::editor(name, ['toolbar' => 'MyTool'])` | CKEditor | -| `image` | `FormField::image(name, ['filemanager' => true])` | filemanager URL | -| `file` | `FormField::file(name, [...])` | filemanager | -| `hidden` | `FormField::hidden(name, value)` | - | -| `lang_section` | `FormField::langSection(name, 'tab', [fields])` | pola per język | - -### Walidacja - -Walidacja jest automatyczna na podstawie właściwości pól: -- `required` - pole wymagane -- `type` = `email` - walidacja formatu e-mail -- `type` = `number` - walidacja liczby -- `type` = `date` - walidacja formatu YYYY-MM-DD - -Dla sekcji językowych walidacja jest powtarzana dla każdego aktywnego języka. - -### Persist (zapamiętywanie danych) - -Gdy `persist = true`: -1. Przy błędzie walidacji dane są zapisywane w `$_SESSION['form_persist'][$formId]` -2. Formularz automatycznie przywraca dane z sesji przy ponownym wyświetleniu -3. Po udanym zapisie sesja jest czyszczona automatycznie przez `FormRequestHandler` - -### Przerabianie istniejących formularzy - -1. **Kontroler** - zamień `view\Xxx::edit()` na `FormEditViewModel` -2. **Repository** - dostosuj `save()` do formatu z `FormRequestHandler` (lub dodaj wsparcie dla obu formatów) -3. **Szablon** - usuń stary szablon lub zostaw jako fallback -4. **Testy** - zaktualizuj testy jeśli zmienił się format danych - - -## Aktualizacja 2026-02-12 - Users - -### Users (migracja kontrolera i repozytorium) -- **NOWE:** `Domain\User\UserRepository` (delete, find, save, checkLogin, logon, details, sendTwofaCode, verifyTwofaCode) -- **NOWE:** `admin\Controllers\UsersController` (view_list, user_edit, user_save, user_delete) -- **UPDATE:** Router `admin\Site` - nowy kontroler DI dla modu�u `Users` -- **UPDATE:** `admin\factory\Users` jako fasada delegujaca do repozytorium -- **UPDATE:** `admin/ajax/users.php` - endpoint `check_login` oparty o `UserRepository` -- Testy po zmianie: 95 tests, 204 assertions -- **UPDATE:** UsersController: `view_list` + `user_edit` migrowane na nowy system list/form (table-list + form-edit) - -## Aktualizacja 2026-02-12 (finalizacja Users) -- Users: pelna migracja na nowa architekture (Domain + DI Controller), bez fallbacku do legacy kontrolera/factory/view. -- `UsersController` obsluguje: `list/view_list`, `user_edit`, `user_save`, `user_delete`, `login_form`, `twofa`. -- Dodano walidacje warunkowa: `twofa_email` wymagany gdy `twofa_enabled = 1`. -- Widoki users migrowane z `grid/gridEdit` na `table-list` i `form-edit`. - -## Aktualizacja 2026-02-12 - Languages -- **NOWE:** `Domain\\Languages\\LanguagesRepository` (languages + translations CRUD/list) -- **NOWE:** `admin\\Controllers\\LanguagesController` (DI) -- **UPDATE:** `admin\\Site` - nowy kontroler DI dla modulu `Languages` -- **UPDATE:** `admin\\factory\\Languages` jako fasada delegujaca do repozytorium -- **UPDATE:** widoki `languages/*` migrowane na `components/table-list` i `components/form-edit` -- **CLEANUP:** usunieto legacy `admin\\controls\\Languages` i `admin\\view\\Languages` -- Testy po zmianie: 130 tests, 301 assertions - -## Aktualizacja 2026-02-12 (Languages final) -- **NOWE:** `Domain\\Languages\\LanguagesRepository` (list/save/delete dla jezykow i tlumaczen) -- **NOWE:** `admin\\Controllers\\LanguagesController` (DI) dla akcji `view_list/list`, `language_*`, `translation_*` -- **UPDATE:** `admin\\factory\\Languages` jako fasada delegujaca do repozytorium -- **CLEANUP:** usunieto legacy `admin\\controls\\Languages` oraz `admin\\view\\Languages` -- **UPDATE:** poprawki globalne `components/table-list` dla krotkich kolumn/filtr�w - -## Aktualizacja 2026-02-12 (ver. 0.255) -- UPDATE: SettingsController, BannerController, DictionariesController, ArticlesController pobieraja liste jezykow przez Domain/Languages/LanguagesRepository (DI) zamiast legacy admin/factory/Languages. -- UPDATE: router DI (admin/Site) przekazuje LanguagesRepository do kontrolerow Articles, Banners, Settings, Dictionaries. -- UPDATE: pozostale aktywne odwolania legacy (admin/controls, admin/factory/Shop*) zostaly przepiete na LanguagesRepository. -- FIX: autoload/admin/factory/class.Languages.php poprawione na restore` - - CLEANUP: usuniete `autoload/admin/controls/class.ArticlesArchive.php`, `autoload/admin/factory/class.ArticlesArchive.php`, `autoload/admin/view/class.ArticlesArchive.php` -- Testy po zmianie: **165 tests, 424 assertions** - -## Plan 2026-02-13 - Refaktoryzacja `/admin/articles/` -- [ ] Przeniesc zaleznosci listy artykulow z `admin\factory\Articles` do `Domain\Article\ArticleRepository` (etykiety stron, operacje pomocnicze). -- [ ] Dodac akcje routowane przez `admin\Controllers\ArticlesController` dla operacji AJAX (`article_image_alt_change`, `article_file_name_change`, `article_image_delete`, `article_file_delete`). -- [ ] Przepiac widok `admin/templates/articles/article-edit.php` z `/admin/ajax.php` na endpointy `/admin/articles/*`. -- [ ] Usunac legacy `admin\view\Articles` i zastapic rekurencje podstron przez `Tpl::view('articles/subpages-list', ...)`. -- [ ] Usunac `admin/ajax/articles.php` oraz odpiac include z `admin/ajax.php`. -- [ ] Przeszukac projekt pod pozostale zaleznosci i uruchomic testy modulu Articles. - -## Aktualizacja 2026-02-13 (ver. 0.261) -- **Articles** - dalsza refaktoryzacja `/admin/articles/` - - UPDATE: `Domain\Article\ArticleRepository` rozszerzone o metody UI/admin: `pagesSummaryForArticles()`, `updateImageAlt()`, `updateFileName()`, `markImageToDelete()`, `markFileToDelete()`. - - UPDATE: `admin\Controllers\ArticlesController` obsluguje nowe akcje routingu: `article_image_alt_change`, `article_file_name_change`, `article_image_delete`, `article_file_delete`. - - UPDATE: lista artykulow (`list`) nie korzysta juz z `admin\factory\Articles::article_pages()`. - - UPDATE: `admin/templates/articles/article-edit.php` przepiete z `/admin/ajax.php?a=article_*` na endpointy `/admin/articles/article_*/`. - - UPDATE: rekurencja podstron w widoku oparta o `Tpl::view('articles/subpages-list', ...)` (bez `admin\view\Articles`). - - CLEANUP: usuniete legacy pliki `autoload/admin/view/class.Articles.php` oraz `admin/ajax/articles.php`; `admin/ajax.php` nie includuje juz `ajax/articles.php`. -- Testy po zmianie: **176 tests, 439 assertions**. - -## Aktualizacja 2026-02-13 (ver. 0.261) -- **Articles (/admin/articles)** - - UPDATE: `Domain\Article\ArticleRepository` rozszerzone o `saveFilesOrder()` oraz zapis `files_order` przy `save()` (eliminuje koniecznosc drugiego zapisu po sortowaniu). - - UPDATE: routing DI (`admin\Site`) rozszerzony o mapowanie `files_order_save -> filesOrderSave`. - - UPDATE: `admin\Controllers\ArticlesController` - nowa akcja AJAX `filesOrderSave`. - - UPDATE: widok `admin/templates/articles/article-edit-custom-script.php` - drag&drop dla listy zalacznikow + hidden input `files_order`. - - UPDATE: potwierdzenia usuwania zdjec i zalacznikow przepiete na `jquery-confirm` ze stylem `table-list-confirm-dialog` (jak na liscie artykulow). - - FIX: dolaczona biblioteka `jquery-impromptu` w widoku edycji artykulu dla kompatybilnosci. -- Testy po zmianie: **178 tests, 443 assertions**. - -## Plan 2026-02-13 - Refaktoryzacja `/admin/pages/` -- [x] Dodac `Domain\Pages\PagesRepository` (CRUD menu/stron, drzewo stron, sortowanie, SEO, operacje AJAX). -- [x] Dodac `admin\Controllers\PagesController` (DI) i przepiac routing `/admin/pages/*` na nowy kontroler. -- [x] Przebudowac widoki `admin/templates/pages/*` tak, aby nie korzystaly z `admin\factory\Pages` i `admin\view\Pages`. -- [x] Przepiac endpointy AJAX z `/admin/ajax.php?a=*` na `/admin/pages/*` (`save_pages_order`, `save_articles_order`, `generate_seo_link`, `cookie_*`). -- [x] Przeszukac i zaktualizowac zaleznosci w innych modulach (`articles`, `layouts`, helpery) powiazane z Pages. -- [x] Usunac legacy klasy/pliki Pages (`autoload/admin/controls/class.Pages.php`, `autoload/admin/view/class.Pages.php`, `autoload/admin/factory/class.Pages.php`, `admin/ajax/pages.php`) po odpieciu zaleznosci. -- [x] Dodac/uzupelnic testy (`PagesRepository`, `PagesController`) i uruchomic testy. - -## Aktualizacja 2026-02-13 - Pages (/admin/pages) -- NOWE: `Domain\Pages\PagesRepository` (CRUD menu/stron, drzewo stron, porzadkowanie, SEO link, URL preview, cookies tree-state). -- NOWE: `admin\Controllers\PagesController` (DI) dla akcji: `view_list/list`, `browse_list`, `pages_url_browser`, `menu_*`, `page_*`, `save_*_order`, `generate_seo_link`, `cookie_*`. -- UPDATE: `/admin/pages/*` dziala bez legacy `admin\controls\Pages` i `admin\view\Pages`. -- UPDATE: widoki `admin/templates/pages/*` przepiete na dane z kontrolera/repozytorium (bez `admin\factory\Pages`). -- UPDATE: endpointy zalezne od Pages w innych modulach (`articles`, `layouts`, `shop-category`, `shop-product`) przepiete z `admin/ajax.php?a=*` na `/admin/pages/*`. -- CLEANUP: usuniete `autoload/admin/controls/class.Pages.php`, `autoload/admin/view/class.Pages.php`, `autoload/admin/factory/class.Pages.php`, `admin/ajax/pages.php`; `admin/ajax.php` nie includuje juz `ajax/pages.php`. -- Testy: **OK (186 tests, 478 assertions)**. - -## Aktualizacja 2026-02-13 - Integrations (/admin/integrations) -- NOWE: `Domain\Integrations\IntegrationsRepository` (settings Apilo/ShopPRO, OAuth, product linking, API fetch lists, product search/create, ShopPRO import). -- NOWE: `admin\Controllers\IntegrationsController` (DI) dla akcji: `apilo_settings`, `apilo_settings_save`, `apilo_authorization`, `get_platform_list`, `get_status_types_list`, `get_carrier_account_list`, `get_payment_types_list`, `apilo_create_product`, `apilo_product_search`, `apilo_product_select_save`, `apilo_product_select_delete`, `shoppro_settings`, `shoppro_settings_save`, `shoppro_product_import`. -- UPDATE: `admin\factory\Integrations` jako fasada delegujaca do `Domain\Integrations\IntegrationsRepository` (tylko Apilo + ShopPRO). -- **CLEANUP: usunieto integracje Sellasist i Baselinker z calego projektu:** - - Usuniete klasy: `admin\controls\Integrations`, `admin\controls\Baselinker`, `admin\factory\Baselinker`, `front\factory\Shop`, `shop\ShopStatus` - - Usuniete szablony: `integrations/sellasist-settings.php`, `integrations/baselinker-settings.php`, `admin/templates/baselinker/` - - Wyczyszczone referencje w: `cron.php`, `cron/cron-xml.php`, `shop\Order`, `admin\controls\ShopStatuses`, `admin\controls\ShopTransport`, `admin\controls\ShopPaymentMethod`, `admin\controls\ShopProduct`, `admin\factory\ShopStatuses`, `admin\factory\ShopTransport`, `admin\factory\ShopProduct`, `front\factory\ShopStatuses`, `front\factory\ShopTransport`, `front\factory\ShopPaymentMethod`, `front\factory\ShopProduct`, `front\factory\ShopOrder`, `shop\Product`, `config.php` - - Wyczyszczone szablony: `shop-statuses/*`, `shop-transport/*`, `shop-payment-method/*`, `shop-product/*`, `site/main-layout.php` -- Testy: **OK (212 tests, 577 assertions)**. - -## Plan 2026-02-13 - Refaktoryzacja `/admin/shop_promotion/` (HITL) -- [x] Etap 1 (analiza i kontrakt): potwierdzic docelowy kontrakt URL i kompatybilnosc wsteczna: - - kontrakt docelowy: tylko `/admin/shop_promotion/list/`, `/admin/shop_promotion/edit/`, `/admin/shop_promotion/save/`, `/admin/shop_promotion/delete/` - - brak kompatybilnosci ze starymi URL i aliasami akcji (`view_list`, `promotion_delete`) -- [x] Etap 2 (Domain): dodac `Domain\Promotion\PromotionRepository`: - - `listForAdmin(filters, sort, dir, page, perPage)` z whitelist sortowania i bind params - - `find(int $id)` + domyslne dane dla nowego formularza - - `save(array $data): ?int` (insert/update, normalizacja switchy, JSON dla kategorii) - - `delete(int $id): bool` - - `categoriesTree(?int $parentId): array` (drzewo kategorii z tlumaczeniami, bez zaleznosci od `admin\factory\ShopCategory`) -- [x] Etap 3 (Admin Controller + routing DI): dodac `admin\Controllers\ShopPromotionController` i przepiac routing: - - rejestracja factory w `admin\Site::$newControllers` pod modulem `ShopPromotion` - - akcje: `list`, `edit`, `save`, `delete` - - zachowac obsluge legacy payload (`values` JSON) oraz obsluge `form-edit` (`$_POST`) -- [x] Etap 4 (widoki): przepiac modul z `grid/gridEdit` na nowe komponenty: - - nowy widok listy oparty o `components/table-list` (filtry: nazwa, aktywny) - - nowy widok edycji oparty o `components/form-edit` (+ pola custom dla drzew kategorii) - - nowe partiale dla drzewa kategorii w module `shop-promotion` (usuniecie zaleznosci od `shop-product/subcategories-list`) - - nowy `shop-promotion/promotion-edit-custom-script.php` (warunkowe pola po `condition_type`, obsluga drzewa kategorii) -- [x] Etap 5 (zaleznosci i cleanup): przeszukac i odpiac legacy zaleznosci: - - menu admin: link kanoniczny na `/admin/shop_promotion/list/` - - usunac legacy pliki po pelnym przepieciu: - - `autoload/admin/controls/class.ShopPromotion.php` - - `autoload/admin/factory/class.ShopPromotion.php` - - `admin/templates/shop-promotion/view-list.php` (grid) - - `admin/templates/shop-promotion/promotion-edit.php` (gridEdit) - - sprawdzic pozostale odwolania `ShopPromotion` i `shop_promotion/view_list` w calym repo -- [x] Etap 6 (testy): dodac/uzupelnic testy: - - `tests/Unit/Domain/Promotion/PromotionRepositoryTest.php` - - `tests/Unit/admin/Controllers/ShopPromotionControllerTest.php` - - uruchomic minimum: nowe testy modulu + pelny `composer test` -- [x] Etap 7 (dokumentacja po wdrozeniu): zaktualizowac: - - `DATABASE_STRUCTURE.md` (dodac `pp_shop_promotion`, jesli nadal brak) - - `PROJECT_STRUCTURE.md` - - `REFACTORING_PLAN.md` (sekcja "Aktualizacja ...") - - `TESTING.md` (nowy wynik suite) - -## Aktualizacja 2026-02-13 (ver. 0.264) -- **ShopPromotion** - migracja `/admin/shop_promotion` na Domain + DI + nowe widoki - - NOWE: `Domain\Promotion\PromotionRepository` (`listForAdmin`, `find`, `save`, `delete`, `categoriesTree`, invalidacja cache aktywnych promocji) - - NOWE: `admin\Controllers\ShopPromotionController` (DI) z akcjami `list`, `edit`, `save`, `delete` - - UPDATE: routing DI (`admin\Site`) rozszerzony o modul `ShopPromotion` - - UPDATE: modul `/admin/shop_promotion/*` dziala na `components/table-list` i `components/form-edit` - - NOWE: widoki/partiale `shop-promotion/promotions-list`, `shop-promotion/promotion-edit`, `shop-promotion/promotion-categories-selector`, `shop-promotion/promotion-categories-tree`, `shop-promotion/promotion-edit-custom-script` - - CLEANUP: usuniete legacy `autoload/admin/controls/class.ShopPromotion.php`, `autoload/admin/factory/class.ShopPromotion.php`, `admin/templates/shop-promotion/view-list.php` - - UPDATE: menu admin przepiete na kanoniczny URL `/admin/shop_promotion/list/` -- Testy po zmianie: **OK (222 tests, 609 assertions)**. - -## Aktualizacja 2026-02-13 (ver. 0.265) -- **ShopPromotion** - stabilizacja po migracji - - UPDATE: dodane `date_from` w `Domain\Promotion\PromotionRepository` (save/find/list/sort) - - UPDATE: `admin\Controllers\ShopPromotionController` rozszerzony o pole `Data od` na formularzu i kolumne `Data od` na liscie - - UPDATE: `shop\Promotion::get_active_promotions()` filtruje aktywnosc po `date_from` i `date_to` - - FIX: zapis edycji promocji nie tworzy nowego rekordu (hidden `id` + fallback `id` z URL) - - TEST: rozszerzono `PromotionRepositoryTest` o asercje `date_from` -- Testy po zmianie: **OK (222 tests, 614 assertions)**. - -## Plan 2026-02-13 - Refaktoryzacja `/admin/shop_coupon/` (HITL) -- [x] Etap 1 (analiza i kontrakt URL/routingu): - - potwierdzic docelowy kontrakt URL: `/admin/shop_coupon/list/`, `/admin/shop_coupon/edit/`, `/admin/shop_coupon/save/`, `/admin/shop_coupon/delete/` - - decyzja: utrzymujemy aliasy legacy (`view_list`, `coupon_edit`, `coupon_save`, `coupon_delete`) w nowym kontrolerze jako kompatybilnosc wsteczna, przy jednoczesnym przejsciu menu i nowych widokow na URL kanoniczne - - sprawdzic mapowanie modulu `ShopCoupon` w `admin\Site` (DI factory + fallback) -- [x] Etap 2 (Domain): - - dodac `Domain\Coupon\CouponRepository`: - - `listForAdmin(filters, sort, dir, page, perPage)` (whitelist sortowania + paginacja) - - `find(int $id)` (domyslne dane dla nowego formularza) - - `save(array $data): ?int` (insert/update, normalizacja switchy, JSON dla `categories`) - - `delete(int $id): bool` - - `categoriesTree(?int $parentId): array` (drzewo kategorii bez zaleznosci od `admin\factory\ShopCategory`) -- [x] Etap 3 (Admin Controller + routing DI): - - dodac `admin\Controllers\ShopCouponController` z akcjami `list`, `edit`, `save`, `delete` - - przepiac routing DI w `admin\Site::$newControllers` dla modulu `ShopCoupon` - - zachowac obsluge legacy payload `values` JSON i nowego payload `$_POST` z `components/form-edit` -- [x] Etap 4 (widoki): - - przepiac liste z `grid` na `components/table-list` (filtry: nazwa, aktywny, uzyty, wyslany) - - przepiac edycje z `gridEdit` na `components/form-edit` - - dodac partiale drzewa kategorii w module `shop-coupon` (usuniecie zaleznosci od `shop-product/subcategories-list`) - - dodac `shop-coupon/coupon-edit-custom-script.php` (obsluga drzewa kategorii i zachowania formularza) -- [x] Etap 5 (cleanup i zaleznosci): - - usunac legacy po pelnym przepieciu: - - `autoload/admin/controls/class.ShopCoupon.php` - - `autoload/admin/factory/class.ShopCoupon.php` - - `admin/templates/shop-coupon/view-list.php` (wersja grid) - - `admin/templates/shop-coupon/coupon-edit.php` (wersja gridEdit) - - przepiac menu admin na kanoniczny URL `/admin/shop_coupon/list/` - - przeszukac repo i usunac pozostale odwolania do `shop_coupon/view_list` i legacy klas `admin\controls\ShopCoupon`, `admin\factory\ShopCoupon` -- [x] Etap 6 (testy): - - dodac `tests/Unit/Domain/Coupon/CouponRepositoryTest.php` - - dodac `tests/Unit/admin/Controllers/ShopCouponControllerTest.php` - - uruchomic testy modulu + pelny `composer test` -- [x] Etap 7 (dokumentacja i release note): - - zaktualizowac `DATABASE_STRUCTURE.md` (dodac `pp_shop_coupon`) - - zaktualizowac `PROJECT_STRUCTURE.md` - - zaktualizowac `REFACTORING_PLAN.md` (sekcja "Aktualizacja ...") - - zaktualizowac `TESTING.md` (nowy wynik suite + nowe testy) - - dopisac wpis w `updates/changelog.php` - -### Tryb HITL dla realizacji -- Po kazdym etapie (1-7) zatrzymanie i krotkie podsumowanie diffu do akceptacji przed kolejnym krokiem. - -### Postep 2026-02-13 (ShopCoupon) -- Etap 2 zakonczony: - - NOWE: `autoload/Domain/Coupon/CouponRepository.php` - - Zakres: `listForAdmin`, `find`, `save`, `delete`, `categoriesTree` - - Walidacja: `php -l` OK -- Etap 3 zakonczony: - - NOWE: `autoload/admin/Controllers/ShopCouponController.php` - - UPDATE: `autoload/admin/class.Site.php` - rejestracja DI factory dla modulu `ShopCoupon` - - Kompatybilnosc: dodane aliasy akcji `view_list`, `coupon_edit`, `coupon_save`, `coupon_delete` - - Walidacja: `php -l` OK -- Etap 4 zakonczony: - - NOWE widoki: `admin/templates/shop-coupon/coupons-list.php`, `admin/templates/shop-coupon/coupon-edit-new.php` - - NOWE partiale: `admin/templates/shop-coupon/coupon-categories-selector.php`, `admin/templates/shop-coupon/coupon-categories-tree.php` - - NOWY skrypt: `admin/templates/shop-coupon/coupon-edit-custom-script.php` - - UPDATE: `ShopCouponController::edit()` buduje `FormEditViewModel` (zakladki ustawienia/kategorie) - - Walidacja: `php -l` OK -- Etap 5 zakonczony: - - CLEANUP: usuniete pliki legacy: - - `autoload/admin/controls/class.ShopCoupon.php` - - `autoload/admin/factory/class.ShopCoupon.php` - - `admin/templates/shop-coupon/view-list.php` - - `admin/templates/shop-coupon/coupon-edit.php` - - UPDATE: menu admin (`admin/templates/site/main-layout.php`) wskazuje kanoniczny URL `/admin/shop_coupon/list/` - - WERYFIKACJA: brak odwolan do `shop_coupon/view_list`, `admin\controls\ShopCoupon`, `admin\factory\ShopCoupon` w kodzie -- Etap 6 zakonczony: - - NOWE testy: - - `tests/Unit/Domain/Coupon/CouponRepositoryTest.php` (8 testow) - - `tests/Unit/admin/Controllers/ShopCouponControllerTest.php` (5 testow) - - Test modulu: `OK (8 tests, 49 assertions)` - - Pelny suite: `OK (235 tests, 682 assertions)` -- Etap 7 zakonczony: - - UPDATE: dokumentacja techniczna zaktualizowana (`DATABASE_STRUCTURE.md`, `PROJECT_STRUCTURE.md`, `TESTING.md`) - - UPDATE: dopisany release note w `updates/changelog.php` (ver. 0.266) - -## Aktualizacja 2026-02-13 (ver. 0.266) -- **ShopCoupon** - migracja `/admin/shop_coupon` na Domain + DI + nowe widoki - - NOWE: `Domain\Coupon\CouponRepository` (`listForAdmin`, `find`, `save`, `delete`, `categoriesTree`) - - NOWE: `admin\Controllers\ShopCouponController` (DI) z akcjami `list`, `edit`, `save`, `delete` - - UPDATE: kompatybilnosc aliasow legacy (`view_list`, `coupon_edit`, `coupon_save`, `coupon_delete`) obslugiwana przez nowy kontroler - - UPDATE: modul `/admin/shop_coupon/*` dziala na `components/table-list` i `components/form-edit` - - NOWE: widoki/partiale `shop-coupon/coupons-list`, `shop-coupon/coupon-edit-new`, `shop-coupon/coupon-categories-selector`, `shop-coupon/coupon-categories-tree`, `shop-coupon/coupon-edit-custom-script` - - CLEANUP: usuniete legacy `autoload/admin/controls/class.ShopCoupon.php`, `autoload/admin/factory/class.ShopCoupon.php`, `admin/templates/shop-coupon/view-list.php`, `admin/templates/shop-coupon/coupon-edit.php` - - UPDATE: menu admin przepiete na kanoniczny URL `/admin/shop_coupon/list/` - - FIX: po akceptacji HITL ujednolicone UI drzewek i checkboxow miedzy kuponami i layoutami (spojne strzalki, brak nieestetycznego focusu, iCheck dla checkboxow) -- Testy po zmianie: **OK (235 tests, 682 assertions)**. diff --git a/admin/templates/shop-statuses/status-edit.php b/admin/templates/shop-statuses/status-edit.php index f4387e8..5fe1611 100644 --- a/admin/templates/shop-statuses/status-edit.php +++ b/admin/templates/shop-statuses/status-edit.php @@ -1,79 +1 @@ - - - apilo_order_status_list as $apilo_status ) -{ - $apilo_status_types_list[ $apilo_status['id'] ] = $apilo_status['name']; -} -ob_start(); -?> -
-
    -
  • Ogólne
  • -
-
-
- 'Status', - 'class' => 'require', - 'name' => 'name', - 'id' => 'name', - 'readonly' => true, - 'value' => $this -> status['status'] - ] );?> - 'Kolor', - 'name' => 'color', - 'id' => 'color', - 'value' => $this -> status['color'] - ] );?> - 'Status z Apilo', - 'name' => 'apilo_status_id', - 'id' => 'apilo_status_id', - 'values' => $apilo_status_types_list, - 'value' => $this -> status['apilo_status_id'] - ] );?> -
-
-
- id = 'status-edit'; -$grid -> gdb_opt = $gdb; -$grid -> include_plugins = true; -$grid -> title = 'Edycja statusu zamówienia'; -$grid -> fields = [ - [ - 'db' => 'id', - 'type' => 'hidden', - 'value' => $this -> status['id'] - ] - ]; -$grid -> actions = [ - 'save' => [ 'url' => '/admin/shop_statuses/status_save/', 'back_url' => '/admin/shop_statuses/view_list/' ], - 'cancel' => [ 'url' => '/admin/shop_statuses/view_list/' ] - ]; -$grid -> external_code = $out; -$grid -> persist_edit = true; -$grid -> id_param = 'id'; - -echo $grid -> draw(); -?> - \ No newline at end of file + $this->form]); ?> diff --git a/admin/templates/shop-statuses/view-list.php b/admin/templates/shop-statuses/view-list.php index 7385332..0f89c5b 100644 --- a/admin/templates/shop-statuses/view-list.php +++ b/admin/templates/shop-statuses/view-list.php @@ -1,38 +1 @@ - apilo_order_status_list as $apilo_status ) -{ - $apilo_order_status_list[ $apilo_status['id'] ] = $apilo_status['name']; -} - -$grid = new \grid( 'pp_shop_statuses' ); -$grid -> gdb_opt = $gdb; -$grid -> debug = true; -$grid -> order = [ 'column' => 'o', 'type' => 'ASC' ]; -$grid -> search = [ - [ 'name' => 'Status', 'db' => 'status', 'type' => 'text' ] - ]; -$grid -> columns_view = [ - [ - 'name' => 'Lp.', - 'th' => [ 'class' => 'g-lp' ], - 'td' => [ 'class' => 'g-center' ], - 'autoincrement' => true - ], [ - 'name' => 'Status', - 'db' => 'status' - ], [ - 'name' => 'Kolor', - 'db' => 'color' - ], [ - 'name' => 'Status Apilo', - 'db' => 'apilo_status_id', - 'replace' => [ 'array' => $apilo_order_status_list ], - ], [ - 'name' => 'Edytuj', - 'action' => [ 'type' => 'edit', 'url' => '/admin/shop_statuses/status_edit/id=[id]' ], - 'th' => [ 'class' => 'g-center', 'style' => 'width: 70px;' ], - 'td' => [ 'class' => 'g-center' ] - ] - ]; -echo $grid -> draw(); \ No newline at end of file + $this->viewModel]); ?> diff --git a/admin/templates/site/main-layout.php b/admin/templates/site/main-layout.php index 847f84d..d2aa5bb 100644 --- a/admin/templates/site/main-layout.php +++ b/admin/templates/site/main-layout.php @@ -73,7 +73,7 @@
  • Rodzaje transportu
  • Metody płatności
  • - Statusy zamówień + Statusy zamówień
  • Kody rabatowe
  • Promocje
  • diff --git a/autoload/Domain/ShopStatus/ShopStatusRepository.php b/autoload/Domain/ShopStatus/ShopStatusRepository.php new file mode 100644 index 0000000..04dbee5 --- /dev/null +++ b/autoload/Domain/ShopStatus/ShopStatusRepository.php @@ -0,0 +1,181 @@ +db = $db; + } + + /** + * @return array{items: array>, total: int} + */ + public function listForAdmin( + array $filters, + string $sortColumn = 'o', + string $sortDir = 'ASC', + int $page = 1, + int $perPage = 15 + ): array { + $allowedSortColumns = [ + 'id' => 'ss.id', + 'status' => 'ss.status', + 'color' => 'ss.color', + 'o' => 'ss.o', + 'apilo_status_id' => 'ss.apilo_status_id', + ]; + + $sortSql = $allowedSortColumns[$sortColumn] ?? 'ss.o'; + $sortDir = strtoupper(trim($sortDir)) === 'DESC' ? 'DESC' : 'ASC'; + $page = max(1, $page); + $perPage = min(self::MAX_PER_PAGE, max(1, $perPage)); + $offset = ($page - 1) * $perPage; + + $where = ['1 = 1']; + $params = []; + + $status = trim((string)($filters['status'] ?? '')); + if ($status !== '') { + if (strlen($status) > 255) { + $status = substr($status, 0, 255); + } + $where[] = 'ss.status LIKE :status'; + $params[':status'] = '%' . $status . '%'; + } + + $whereSql = implode(' AND ', $where); + + $sqlCount = " + SELECT COUNT(0) + FROM pp_shop_statuses AS ss + WHERE {$whereSql} + "; + + $stmtCount = $this->db->query($sqlCount, $params); + $countRows = $stmtCount ? $stmtCount->fetchAll() : []; + $total = isset($countRows[0][0]) ? (int)$countRows[0][0] : 0; + + $sql = " + SELECT + ss.id, + ss.status, + ss.color, + ss.o, + ss.apilo_status_id + FROM pp_shop_statuses AS ss + WHERE {$whereSql} + ORDER BY {$sortSql} {$sortDir}, ss.id ASC + LIMIT {$perPage} OFFSET {$offset} + "; + + $stmt = $this->db->query($sql, $params); + $items = $stmt ? $stmt->fetchAll() : []; + + if (!is_array($items)) { + $items = []; + } + + foreach ($items as &$item) { + $item['id'] = (int)($item['id'] ?? 0); + $item['apilo_status_id'] = $item['apilo_status_id'] !== null + ? (int)$item['apilo_status_id'] + : null; + $item['o'] = (int)($item['o'] ?? 0); + } + unset($item); + + return [ + 'items' => $items, + 'total' => $total, + ]; + } + + public function find(int $statusId): ?array + { + if ($statusId < 0) { + return null; + } + + $status = $this->db->get('pp_shop_statuses', '*', ['id' => $statusId]); + if (!is_array($status)) { + return null; + } + + $status['id'] = (int)($status['id'] ?? 0); + $status['apilo_status_id'] = $status['apilo_status_id'] !== null + ? (int)$status['apilo_status_id'] + : null; + $status['o'] = (int)($status['o'] ?? 0); + + return $status; + } + + public function save(int $statusId, array $data): int + { + if ($statusId < 0) { + return 0; + } + + $row = [ + 'color' => trim((string)($data['color'] ?? '')), + 'apilo_status_id' => isset($data['apilo_status_id']) && $data['apilo_status_id'] !== '' + ? (int)$data['apilo_status_id'] + : null, + ]; + + $this->db->update('pp_shop_statuses', $row, ['id' => $statusId]); + + return $statusId; + } + + /** + * Pobiera Apilo status ID dla danego statusu sklepowego. + * Odpowiednik front\factory\ShopStatuses::get_apilo_status_id() + */ + public function getApiloStatusId(int $statusId): ?int + { + $value = $this->db->get('pp_shop_statuses', 'apilo_status_id', ['id' => $statusId]); + return $value !== null && $value !== false ? (int)$value : null; + } + + /** + * Pobiera shop status ID na podstawie ID statusu integracji. + * Odpowiednik front\factory\ShopStatuses::get_shop_status_by_integration_status_id() + */ + public function getByIntegrationStatusId(string $integration, int $integrationStatusId): ?int + { + if ($integration === 'apilo') { + $value = $this->db->get('pp_shop_statuses', 'id', [ + 'apilo_status_id' => $integrationStatusId, + ]); + return $value !== null && $value !== false ? (int)$value : null; + } + + return null; + } + + /** + * Zwraca liste wszystkich statusow (id => nazwa) posortowanych wg kolejnosci. + * Odpowiednik shop\Order::order_statuses() + */ + public function allStatuses(): array + { + $results = $this->db->select('pp_shop_statuses', ['id', 'status'], [ + 'ORDER' => ['o' => 'ASC'], + ]); + + $statuses = []; + if (is_array($results)) { + foreach ($results as $row) { + $statuses[(int)$row['id']] = $row['status']; + } + } + + return $statuses; + } +} diff --git a/autoload/admin/Controllers/ShopStatusesController.php b/autoload/admin/Controllers/ShopStatusesController.php new file mode 100644 index 0000000..1845cfe --- /dev/null +++ b/autoload/admin/Controllers/ShopStatusesController.php @@ -0,0 +1,260 @@ +repository = $repository; + } + + public function list(): string + { + $sortableColumns = ['id', 'status', 'color', 'o', 'apilo_status_id']; + $filterDefinitions = [ + [ + 'key' => 'status', + 'label' => 'Status', + 'type' => 'text', + ], + ]; + + $listRequest = \admin\Support\TableListRequestFactory::fromRequest( + $filterDefinitions, + $sortableColumns, + 'o' + ); + + $sortDir = $listRequest['sortDir']; + if (trim((string)\S::get('sort')) === '') { + $sortDir = 'ASC'; + } + + $result = $this->repository->listForAdmin( + $listRequest['filters'], + $listRequest['sortColumn'], + $sortDir, + $listRequest['page'], + $listRequest['perPage'] + ); + + $apiloStatusList = $this->getApiloStatusList(); + + $rows = []; + $lp = ($listRequest['page'] - 1) * $listRequest['perPage'] + 1; + foreach ($result['items'] as $item) { + $id = (int)($item['id'] ?? 0); + $statusName = trim((string)($item['status'] ?? '')); + $color = trim((string)($item['color'] ?? '')); + $apiloStatusId = $item['apilo_status_id'] ?? null; + + $apiloStatusLabel = ''; + if ($apiloStatusId !== null && isset($apiloStatusList[$apiloStatusId])) { + $apiloStatusLabel = $apiloStatusList[$apiloStatusId]; + } + + $colorHtml = $color !== '' + ? ' ' . htmlspecialchars($color, ENT_QUOTES, 'UTF-8') + : '-'; + + $rows[] = [ + 'lp' => $lp++ . '.', + 'status' => '' . htmlspecialchars($statusName, ENT_QUOTES, 'UTF-8') . '', + 'color' => $colorHtml, + 'apilo_status' => htmlspecialchars($apiloStatusLabel, ENT_QUOTES, 'UTF-8'), + '_actions' => [ + [ + 'label' => 'Edytuj', + 'url' => '/admin/shop_statuses/edit/id=' . $id, + 'class' => 'btn btn-xs btn-primary', + ], + ], + ]; + } + + $total = (int)$result['total']; + $totalPages = max(1, (int)ceil($total / $listRequest['perPage'])); + + $viewModel = new PaginatedTableViewModel( + [ + ['key' => 'lp', 'label' => 'Lp.', 'class' => 'text-center', 'sortable' => false], + ['key' => 'status', 'sort_key' => 'status', 'label' => 'Status', 'sortable' => true, 'raw' => true], + ['key' => 'color', 'sort_key' => 'color', 'label' => 'Kolor', 'sortable' => true, 'raw' => true], + ['key' => 'apilo_status', 'sort_key' => 'apilo_status_id', 'label' => 'Status Apilo', 'sortable' => true], + ], + $rows, + $listRequest['viewFilters'], + [ + 'column' => $listRequest['sortColumn'], + 'dir' => $sortDir, + ], + [ + 'page' => $listRequest['page'], + 'per_page' => $listRequest['perPage'], + 'total' => $total, + 'total_pages' => $totalPages, + ], + array_merge($listRequest['queryFilters'], [ + 'sort' => $listRequest['sortColumn'], + 'dir' => $sortDir, + 'per_page' => $listRequest['perPage'], + ]), + $listRequest['perPageOptions'], + $sortableColumns, + '/admin/shop_statuses/list/', + 'Brak danych w tabeli.' + ); + + return \Tpl::view('shop-statuses/view-list', [ + 'viewModel' => $viewModel, + ]); + } + + public function edit(): string + { + $status = $this->repository->find((int)\S::get('id')); + if ($status === null) { + \S::alert('Status nie zostal znaleziony.'); + header('Location: /admin/shop_statuses/list/'); + exit; + } + + $apiloStatusList = $this->getApiloStatusList(); + + return \Tpl::view('shop-statuses/status-edit', [ + 'form' => $this->buildFormViewModel($status, $apiloStatusList), + ]); + } + + public function save(): void + { + $legacyValues = \S::get('values'); + + if ($legacyValues) { + $values = json_decode((string)$legacyValues, true); + $response = [ + 'status' => 'error', + 'msg' => 'Podczas zapisywania statusu wystapil blad. Prosze sprobowac ponownie.', + ]; + + if (is_array($values)) { + $statusId = (int)($values['id'] ?? 0); + $id = $this->repository->save($statusId, $values); + if ($id !== null && $id >= 0) { + $response = [ + 'status' => 'ok', + 'msg' => 'Status zostal zapisany.', + 'id' => (int)$id, + ]; + } + } + + echo json_encode($response); + exit; + } + + $payload = $_POST; + $statusId = isset($payload['id']) && $payload['id'] !== '' ? (int)$payload['id'] : null; + if ($statusId === null) { + $statusId = (int)\S::get('id'); + } + + $id = $this->repository->save($statusId, $payload); + if ($id !== null && $id >= 0) { + echo json_encode([ + 'success' => true, + 'id' => (int)$id, + 'message' => 'Status zostal zapisany.', + ]); + exit; + } + + echo json_encode([ + 'success' => false, + 'errors' => ['general' => 'Podczas zapisywania statusu wystapil blad.'], + ]); + exit; + } + + private function buildFormViewModel(array $status, array $apiloStatusList): FormEditViewModel + { + $id = (int)($status['id'] ?? 0); + + $apiloOptions = ['' => '--- wybierz status apilo.com ---']; + foreach ($apiloStatusList as $apiloId => $apiloName) { + $apiloOptions[$apiloId] = $apiloName; + } + + $data = [ + 'id' => $id, + 'status' => (string)($status['status'] ?? ''), + 'color' => (string)($status['color'] ?? ''), + 'apilo_status_id' => $status['apilo_status_id'] ?? '', + ]; + + $fields = [ + FormField::hidden('id', $id), + FormField::text('status', [ + 'label' => 'Status', + 'tab' => 'settings', + 'readonly' => true, + ]), + FormField::color('color', [ + 'label' => 'Kolor', + 'tab' => 'settings', + ]), + FormField::select('apilo_status_id', [ + 'label' => 'Status z Apilo', + 'tab' => 'settings', + 'options' => $apiloOptions, + ]), + ]; + + $tabs = [ + new FormTab('settings', 'Ustawienia', 'fa-wrench'), + ]; + + $actionUrl = '/admin/shop_statuses/save/id=' . $id; + $actions = [ + FormAction::save($actionUrl, '/admin/shop_statuses/list/'), + FormAction::cancel('/admin/shop_statuses/list/'), + ]; + + return new FormEditViewModel( + 'status-edit', + 'Edycja statusu zamowienia', + $data, + $fields, + $tabs, + $actions, + 'POST', + $actionUrl, + '/admin/shop_statuses/list/', + true, + ['id' => $id] + ); + } + + private function getApiloStatusList(): array + { + $list = []; + $raw = @unserialize(\admin\factory\Integrations::apilo_settings('status-types-list')); + if (is_array($raw)) { + foreach ($raw as $apiloStatus) { + if (isset($apiloStatus['id'], $apiloStatus['name'])) { + $list[(int)$apiloStatus['id']] = (string)$apiloStatus['name']; + } + } + } + return $list; + } +} diff --git a/autoload/admin/Support/Forms/FormFieldRenderer.php b/autoload/admin/Support/Forms/FormFieldRenderer.php index 9e99461..6bfcfd9 100644 --- a/autoload/admin/Support/Forms/FormFieldRenderer.php +++ b/autoload/admin/Support/Forms/FormFieldRenderer.php @@ -308,6 +308,36 @@ class FormFieldRenderer 'value="' . htmlspecialchars($value ?? '') . '">'; } + /** + * Renderuje pole koloru (color picker + text input) + */ + public function renderColor(FormField $field): string + { + $value = $this->form->getFieldValue($field); + $error = $this->form->getError($field->name); + $colorValue = htmlspecialchars($value ?? '#000000', ENT_QUOTES, 'UTF-8'); + $fieldName = htmlspecialchars($field->name, ENT_QUOTES, 'UTF-8'); + $fieldId = htmlspecialchars($field->id, ENT_QUOTES, 'UTF-8'); + $label = htmlspecialchars($field->label, ENT_QUOTES, 'UTF-8'); + + $html = '
    '; + $html .= ''; + $html .= '
    '; + $html .= '
    '; + $html .= ''; + $html .= ''; + $html .= '
    '; + $html .= '
    '; + $html .= '
    '; + $html .= ''; + + return $this->wrapWithError($html, $error); + } + public function renderCustom(FormField $field): string { return (string)($field->customHtml ?? ''); diff --git a/autoload/admin/ViewModels/Forms/FormField.php b/autoload/admin/ViewModels/Forms/FormField.php index cc07788..075fa91 100644 --- a/autoload/admin/ViewModels/Forms/FormField.php +++ b/autoload/admin/ViewModels/Forms/FormField.php @@ -268,6 +268,21 @@ class FormField ); } + public static function color(string $name, array $config = []): self + { + return new self( + $name, + FormFieldType::COLOR, + $config['label'] ?? '', + $config['value'] ?? null, + $config['tab'] ?? 'default', + $config['required'] ?? false, + $config['attributes'] ?? [], + [], + $config['help'] ?? null + ); + } + public static function hidden(string $name, $value = null): self { return new self( diff --git a/autoload/admin/ViewModels/Forms/FormFieldType.php b/autoload/admin/ViewModels/Forms/FormFieldType.php index 579c15d..77194e2 100644 --- a/autoload/admin/ViewModels/Forms/FormFieldType.php +++ b/autoload/admin/ViewModels/Forms/FormFieldType.php @@ -21,4 +21,5 @@ class FormFieldType public const HIDDEN = 'hidden'; public const LANG_SECTION = 'lang_section'; public const CUSTOM = 'custom'; + public const COLOR = 'color'; } diff --git a/autoload/admin/class.Site.php b/autoload/admin/class.Site.php index 23b4371..34aea29 100644 --- a/autoload/admin/class.Site.php +++ b/autoload/admin/class.Site.php @@ -332,6 +332,13 @@ class Site new \Domain\Integrations\IntegrationsRepository( $mdb ) ); }, + 'ShopStatuses' => function() { + global $mdb; + + return new \admin\Controllers\ShopStatusesController( + new \Domain\ShopStatus\ShopStatusRepository( $mdb ) + ); + }, ]; return self::$newControllers; diff --git a/autoload/admin/controls/class.ShopStatuses.php b/autoload/admin/controls/class.ShopStatuses.php deleted file mode 100644 index ed89927..0000000 --- a/autoload/admin/controls/class.ShopStatuses.php +++ /dev/null @@ -1,34 +0,0 @@ - 'error', 'msg' => 'Podczas zapisywania statusu wystąpił błąd. Proszę spróbować ponownie.' ]; - $values = json_decode( \S::get( 'values' ), true ); - - if ( $id = \admin\factory\ShopStatuses::status_save( $values['id'], $values['color'], $values['apilo_status_id'] ) ) - $response = [ 'status' => 'ok', 'msg' => 'Status został zapisany.', 'id' => $id ]; - - echo json_encode( $response ); - exit; - } - - // status_edit - public static function status_edit() - { - return \Tpl::view( 'shop-statuses/status-edit', [ - 'status' => \admin\factory\ShopStatuses::get_status( \S::get( 'id' ) ), - 'apilo_order_status_list' => unserialize( \admin\factory\Integrations::apilo_settings( 'status-types-list' ) ), - ] ); - } - - static public function view_list() - { - return \Tpl::view( 'shop-statuses/view-list', [ - 'apilo_order_status_list' => unserialize( \admin\factory\Integrations::apilo_settings( 'status-types-list' ) ), - ] ); - } -} \ No newline at end of file diff --git a/autoload/admin/factory/class.Integrations.php b/autoload/admin/factory/class.Integrations.php index 487ee56..1550289 100644 --- a/autoload/admin/factory/class.Integrations.php +++ b/autoload/admin/factory/class.Integrations.php @@ -4,7 +4,7 @@ namespace admin\factory; /** * Fasada kompatybilnosci wstecznej. * Deleguje do Domain\Integrations\IntegrationsRepository. - * Uzywane przez: cron.php, shop\Order, admin\controls\ShopStatuses, admin\controls\ShopTransport, admin\controls\ShopPaymentMethod, admin\controls\ShopProduct. + * Uzywane przez: cron.php, shop\Order, admin\Controllers\ShopStatusesController, admin\controls\ShopTransport, admin\controls\ShopPaymentMethod, admin\controls\ShopProduct. */ class Integrations { diff --git a/autoload/admin/factory/class.ShopStatuses.php b/autoload/admin/factory/class.ShopStatuses.php deleted file mode 100644 index 8a45a62..0000000 --- a/autoload/admin/factory/class.ShopStatuses.php +++ /dev/null @@ -1,24 +0,0 @@ - get( 'pp_shop_statuses', '*', [ 'id' => $id ] ); - } - - // status_save - public static function status_save( $status_id, $color, $apilo_status_id ) - { - global $mdb; - - $mdb -> update( 'pp_shop_statuses', [ - 'color' => $color, - 'apilo_status_id' => $apilo_status_id ? $apilo_status_id : null, - ], [ 'id' => $status_id ] ); - - return $status_id; - } -} \ No newline at end of file diff --git a/autoload/front/factory/class.ShopStatuses.php b/autoload/front/factory/class.ShopStatuses.php index d6fd821..0e20377 100644 --- a/autoload/front/factory/class.ShopStatuses.php +++ b/autoload/front/factory/class.ShopStatuses.php @@ -1,4 +1,4 @@ - get( 'pp_shop_statuses', 'apilo_status_id', [ 'id' => $status_id ] ); + $repo = new \Domain\ShopStatus\ShopStatusRepository( $mdb ); + return $repo->getApiloStatusId( (int)$status_id ); } // get_shop_status_by_integration_status_id static public function get_shop_status_by_integration_status_id( $integration, $integration_status_id ) { global $mdb; - - if ( $integration == 'apilo' ) - return $mdb -> get( 'pp_shop_statuses', 'id', [ 'apilo_status_id' => $integration_status_id ] ); + $repo = new \Domain\ShopStatus\ShopStatusRepository( $mdb ); + return $repo->getByIntegrationStatusId( (string)$integration, (int)$integration_status_id ); } } \ No newline at end of file diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md new file mode 100644 index 0000000..c67c7f4 --- /dev/null +++ b/docs/CHANGELOG.md @@ -0,0 +1,315 @@ +# Changelog shopPRO + +Logi zmian z migracji na Domain-Driven Architecture. Najnowsze na gorze. + +--- + +## ver. 0.267 (2026-02-14) - ShopStatuses + +- **ShopStatuses** - migracja `/admin/shop_statuses` na Domain + DI + nowe widoki + - NOWE: `Domain\ShopStatus\ShopStatusRepository` (`listForAdmin`, `find`, `save`, `getApiloStatusId`, `getByIntegrationStatusId`, `allStatuses`) + - NOWE: `admin\Controllers\ShopStatusesController` (DI) z akcjami `list`, `edit`, `save` (bez aliasow legacy) + - NOWE: typ pola `FormFieldType::COLOR` + `FormField::color()` + `FormFieldRenderer::renderColor()` (color picker HTML5 zsynchronizowany z polem tekstowym) + - UPDATE: modul `/admin/shop_statuses/*` dziala na `components/table-list` i `components/form-edit` + - UPDATE: `front\factory\ShopStatuses` jako fasada delegujaca do `Domain\ShopStatus\ShopStatusRepository` + - UPDATE: menu admin przepiete na kanoniczny URL `/admin/shop_statuses/list/` + - CLEANUP: usuniete legacy `autoload/admin/controls/class.ShopStatuses.php`, `autoload/admin/factory/class.ShopStatuses.php` + - UWAGA: statusy maja ID od 0 - kluczowe dla walidacji (find/save uzywaja `$id < 0`) +- Testy: **OK (254 tests, 736 assertions)** + +--- + +## ver. 0.266 (2026-02-13) - ShopCoupon + +- **ShopCoupon** - migracja `/admin/shop_coupon` na Domain + DI + nowe widoki + - NOWE: `Domain\Coupon\CouponRepository` (`listForAdmin`, `find`, `save`, `delete`, `categoriesTree`) + - NOWE: `admin\Controllers\ShopCouponController` (DI) z akcjami `list`, `edit`, `save`, `delete` + - UPDATE: kompatybilnosc aliasow legacy (`view_list`, `coupon_edit`, `coupon_save`, `coupon_delete`) obslugiwana przez nowy kontroler + - UPDATE: modul `/admin/shop_coupon/*` dziala na `components/table-list` i `components/form-edit` + - NOWE: widoki/partiale `shop-coupon/coupons-list`, `shop-coupon/coupon-edit-new`, `shop-coupon/coupon-categories-selector`, `shop-coupon/coupon-categories-tree`, `shop-coupon/coupon-edit-custom-script` + - CLEANUP: usuniete legacy `autoload/admin/controls/class.ShopCoupon.php`, `autoload/admin/factory/class.ShopCoupon.php`, `admin/templates/shop-coupon/view-list.php`, `admin/templates/shop-coupon/coupon-edit.php` + - UPDATE: menu admin przepiete na kanoniczny URL `/admin/shop_coupon/list/` + - FIX: ujednolicone UI drzewek i checkboxow miedzy kuponami i layoutami +- Testy: **OK (235 tests, 682 assertions)** + +--- + +## ver. 0.265 (2026-02-13) - ShopPromotion poprawki + +- **ShopPromotion** - stabilizacja po migracji + - UPDATE: dodane `date_from` w `Domain\Promotion\PromotionRepository` (save/find/list/sort) + - UPDATE: `admin\Controllers\ShopPromotionController` rozszerzony o pole `Data od` na formularzu i kolumne `Data od` na liscie + - UPDATE: `shop\Promotion::get_active_promotions()` filtruje aktywnosc po `date_from` i `date_to` + - FIX: zapis edycji promocji nie tworzy nowego rekordu (hidden `id` + fallback `id` z URL) + - TEST: rozszerzono `PromotionRepositoryTest` o asercje `date_from` +- Testy: **OK (222 tests, 614 assertions)** + +--- + +## ver. 0.264 (2026-02-13) - ShopPromotion + +- **ShopPromotion** - migracja `/admin/shop_promotion` na Domain + DI + nowe widoki + - NOWE: `Domain\Promotion\PromotionRepository` (`listForAdmin`, `find`, `save`, `delete`, `categoriesTree`, invalidacja cache aktywnych promocji) + - NOWE: `admin\Controllers\ShopPromotionController` (DI) z akcjami `list`, `edit`, `save`, `delete` + - UPDATE: routing DI (`admin\Site`) rozszerzony o modul `ShopPromotion` + - UPDATE: modul `/admin/shop_promotion/*` dziala na `components/table-list` i `components/form-edit` + - NOWE: widoki/partiale `shop-promotion/promotions-list`, `shop-promotion/promotion-edit`, `shop-promotion/promotion-categories-selector`, `shop-promotion/promotion-categories-tree`, `shop-promotion/promotion-edit-custom-script` + - CLEANUP: usuniete legacy `autoload/admin/controls/class.ShopPromotion.php`, `autoload/admin/factory/class.ShopPromotion.php`, `admin/templates/shop-promotion/view-list.php` + - UPDATE: menu admin przepiete na kanoniczny URL `/admin/shop_promotion/list/` +- Testy: **OK (222 tests, 609 assertions)** + +--- + +## ver. 0.263 (2026-02-13) - Integrations + cleanup Sellasist/Baselinker + +- NOWE: `Domain\Integrations\IntegrationsRepository` (settings Apilo/ShopPRO, OAuth, product linking, API fetch) +- NOWE: `admin\Controllers\IntegrationsController` (DI) dla akcji Apilo i ShopPRO +- UPDATE: `admin\factory\Integrations` jako fasada delegujaca do repozytorium +- **CLEANUP: usunieto integracje Sellasist i Baselinker z calego projektu:** + - Usuniete klasy: `admin\controls\Integrations`, `admin\controls\Baselinker`, `admin\factory\Baselinker`, `front\factory\Shop`, `shop\ShopStatus` + - Usuniete szablony: `integrations/sellasist-settings.php`, `integrations/baselinker-settings.php`, `admin/templates/baselinker/` + - Wyczyszczone referencje w: `cron.php`, `cron/cron-xml.php`, `shop\Order`, kontrolery/factory/front Shop* +- Testy: **OK (212 tests, 577 assertions)** + +--- + +## ver. 0.262 (2026-02-13) - Pages + +- NOWE: `Domain\Pages\PagesRepository` (CRUD menu/stron, drzewo stron, sortowanie, SEO) +- NOWE: `admin\Controllers\PagesController` (DI) dla akcji menu/page/AJAX +- UPDATE: widoki `admin/templates/pages/*` przepiete na dane z kontrolera/repozytorium +- UPDATE: endpointy AJAX przepiete z `admin/ajax.php?a=*` na `/admin/pages/*` +- CLEANUP: usuniete legacy `controls/Pages`, `view/Pages`, `factory/Pages`, `ajax/pages.php` +- Testy: **OK (186 tests, 478 assertions)** + +--- + +## ver. 0.261 (2026-02-13) - Articles (dalsza refaktoryzacja) + +- UPDATE: `Domain\Article\ArticleRepository` rozszerzone o metody UI/admin i `saveFilesOrder()` +- UPDATE: `admin\Controllers\ArticlesController` obsluguje AJAX: `article_image_alt_change`, `article_file_name_change`, `article_image_delete`, `article_file_delete`, `filesOrderSave` +- UPDATE: lista artykulow nie korzysta juz z `admin\factory\Articles::article_pages()` +- UPDATE: widok edycji przepiety z `/admin/ajax.php` na `/admin/articles/*` +- UPDATE: drag&drop sortowania listy zalacznikow +- CLEANUP: usuniete `autoload/admin/view/class.Articles.php` i `admin/ajax/articles.php` +- Testy: **OK (178 tests, 443 assertions)** + +--- + +## ver. 0.260 (2026-02-12) - ArticlesArchive + +- NOWE: `admin\Controllers\ArticlesArchiveController` (DI) +- UPDATE: `Domain\Article\ArticleRepository` rozszerzone o `listArchivedForAdmin()`, `restore()`, `deletePermanently()` +- UPDATE: `/admin/articles_archive/view_list/` migrowane na `components/table-list` +- CLEANUP: usuniete legacy `controls/ArticlesArchive`, `factory/ArticlesArchive`, `view/ArticlesArchive` +- Testy: **OK (165 tests, 424 assertions)** + +--- + +## ver. 0.259 (2026-02-12) - Scontainers + +- NOWE: `Domain\Scontainers\ScontainersRepository` (listForAdmin, find, save, delete, detailsForLanguage) +- NOWE: `admin\Controllers\ScontainersController` (DI) +- UPDATE: `/admin/scontainers/*` migrowane na `components/table-list` i `components/form-edit` +- UPDATE: `admin\factory\Scontainers` i `front\factory\Scontainers` jako fasady +- CLEANUP: usuniete `controls/Scontainers`, `view/Scontainers` +- Testy: **OK (158 tests, 397 assertions)** + +--- + +## ver. 0.258 (2026-02-12) - Newsletter (stabilizacja) + +- UPDATE: tymczasowo wylaczono flow `prepare/send/preview` (wymaga przebudowy) +- UPDATE: tymczasowo wylaczono modul `Szablony uzytkownika` +- UPDATE: aktywna obsluga tylko szablonow administracyjnych (`is_admin = 1`) +- CLEANUP: usuniete nieuzywane widoki `prepare.php`, `preview.php`, `email-templates-user.php` + +--- + +## ver. 0.257 (2026-02-12) - Newsletter + +- NOWE: `Domain\Newsletter\NewsletterRepository` (subskrybenci, szablony, ustawienia, kolejka wysylki) +- NOWE: `Domain\Newsletter\NewsletterPreviewRenderer` (render podgladu) +- NOWE: `admin\Controllers\NewsletterController` (DI) +- UPDATE: `/admin/newsletter/*` migrowane na `components/table-list` i `components/form-edit` +- UPDATE: `admin\factory\Newsletter` jako fasada; `front\factory\Newsletter` bez `admin\view\Newsletter` +- CLEANUP: usuniete `controls/Newsletter`, `view/Newsletter` +- Testy: **OK (150 tests, 372 assertions)** + +--- + +## ver. 0.256 (2026-02-12) - Layouts + +- NOWE: `Domain\Layouts\LayoutsRepository` (find, save, delete, listForAdmin, menusWithPages, categoriesTree) +- NOWE: `admin\Controllers\LayoutsController` (DI) +- UPDATE: lista `/admin/layouts/view_list/` migrowana na `components/table-list` +- UPDATE: widok `layouts/layout-edit` korzysta z danych z repozytorium +- NOWE: partial `admin/templates/layouts/subcategories-list.php` +- UPDATE: `Domain\Languages\LanguagesRepository::defaultLanguageId()` jako wspolna metoda +- UPDATE: `ArticlesController` korzysta z `LayoutsRepository` (DI) +- CLEANUP: usuniete `controls/Layouts`, `view/Layouts`; `factory/Layouts` jako fasada +- Testy: **OK (141 tests, 336 assertions)** + +--- + +## ver. 0.255 (2026-02-12) - Languages DI cleanup + +- UPDATE: SettingsController, BannerController, DictionariesController, ArticlesController pobieraja liste jezykow przez `Domain/Languages/LanguagesRepository` (DI) +- UPDATE: router DI przekazuje `LanguagesRepository` do kontrolerow +- UPDATE: legacy `admin/controls`, `admin/factory/Shop*` przepiete na `LanguagesRepository` +- FIX: `admin/factory/class.Languages.php` poprawione na `repository->find($id); + $languages = \admin\factory\Languages::languages_list(); + + $viewModel = new FormEditViewModel( + formId: 'banner-edit', + title: 'Edycja banera', + data: $banner, + tabs: [ + new FormTab('settings', 'Ustawienia', 'fa-wrench'), + new FormTab('content', 'Zawartość', 'fa-file'), + ], + fields: [ + // Zakładka Ustawienia + FormField::text('name', [ + 'label' => 'Nazwa', + 'tab' => 'settings', + 'required' => true, + ]), + FormField::switch('status', [ + 'label' => 'Aktywny', + 'tab' => 'settings', + ]), + FormField::date('date_start', [ + 'label' => 'Data rozpoczęcia', + 'tab' => 'settings', + ]), + + // Sekcja językowa w zakładce Zawartość + FormField::langSection('translations', 'content', [ + FormField::image('src', ['label' => 'Obraz']), + FormField::text('url', ['label' => 'Url']), + FormField::editor('text', ['label' => 'Treść']), + ]), + ], + actions: [ + FormAction::save('/admin/banners/save', '/admin/banners'), + FormAction::cancel('/admin/banners'), + ], + languages: $languages, + persist: true, + ); + + return \Tpl::view('components/form-edit', ['form' => $viewModel]); + } + + public function save(): void + { + $formHandler = new FormRequestHandler(); + $viewModel = $this->buildFormViewModel(); // jak w edit() + + $result = $formHandler->handleSubmit($viewModel, $_POST); + + if (!$result['success']) { + // Błędy walidacji - zapisane automatycznie do sesji + echo json_encode(['success' => false, 'errors' => $result['errors']]); + exit; + } + + // Sukces - persist wyczyszczony automatycznie + $this->repository->save($result['data']); + echo json_encode(['success' => true]); + exit; + } +} +``` + +## Dostępne typy pól + +| Typ | Metoda | Opcje | +|-----|--------|-------| +| `text` | `FormField::text(name, ['label' => '...', 'required' => true])` | placeholder, help | +| `number` | `FormField::number(name, [...])` | - | +| `email` | `FormField::email(name, [...])` | walidacja formatu | +| `password` | `FormField::password(name, [...])` | - | +| `date` | `FormField::date(name, [...])` | datetimepicker | +| `datetime` | `FormField::datetime(name, [...])` | datetimepicker z czasem | +| `switch` | `FormField::switch(name, [...])` | checked (bool) | +| `select` | `FormField::select(name, ['options' => [...]])` | options: [key => label] | +| `textarea` | `FormField::textarea(name, ['rows' => 4])` | rows | +| `editor` | `FormField::editor(name, ['toolbar' => 'MyTool'])` | CKEditor | +| `image` | `FormField::image(name, ['filemanager' => true])` | filemanager URL | +| `file` | `FormField::file(name, [...])` | filemanager | +| `hidden` | `FormField::hidden(name, value)` | - | +| `color` | `FormField::color(name, ['label' => '...'])` | HTML5 color picker + text input | +| `lang_section` | `FormField::langSection(name, 'tab', [fields])` | pola per język | + +## Walidacja + +Walidacja jest automatyczna na podstawie właściwości pól: +- `required` - pole wymagane +- `type` = `email` - walidacja formatu e-mail +- `type` = `number` - walidacja liczby +- `type` = `date` - walidacja formatu YYYY-MM-DD + +Dla sekcji językowych walidacja jest powtarzana dla każdego aktywnego języka. + +## Persist (zapamiętywanie danych) + +Gdy `persist = true`: +1. Przy błędzie walidacji dane są zapisywane w `$_SESSION['form_persist'][$formId]` +2. Formularz automatycznie przywraca dane z sesji przy ponownym wyświetleniu +3. Po udanym zapisie sesja jest czyszczona automatycznie przez `FormRequestHandler` + +## Przerabianie istniejących formularzy + +1. **Kontroler** - zamień `view\Xxx::edit()` na `FormEditViewModel` +2. **Repository** - dostosuj `save()` do formatu z `FormRequestHandler` (lub dodaj wsparcie dla obu formatów) +3. **Szablon** - usuń stary szablon lub zostaw jako fallback +4. **Testy** - zaktualizuj testy jeśli zmienił się format danych + +--- +*Dokument aktualizowany: 2026-02-14* diff --git a/docs/PROJECT_STRUCTURE.md b/docs/PROJECT_STRUCTURE.md new file mode 100644 index 0000000..eed9acf --- /dev/null +++ b/docs/PROJECT_STRUCTURE.md @@ -0,0 +1,254 @@ +# Struktura Projektu shopPRO + +Dokumentacja struktury projektu shopPRO do szybkiego odniesienia. + +## System Cache (Redis) + +### Klasy odpowiedzialne za cache + +#### RedisConnection +- **Plik:** `autoload/class.RedisConnection.php` +- **Opis:** Singleton zarządzający połączeniem z Redis +- **Metody:** + - `getInstance()` - pobiera instancję połączenia + - `getConnection()` - zwraca obiekt Redis + +#### CacheHandler +- **Plik:** `autoload/class.CacheHandler.php` +- **Opis:** Handler do obsługi cache Redis +- **Metody:** + - `get($key)` - pobiera wartość z cache + - `set($key, $value, $ttl = 86400)` - zapisuje wartość do cache + - `exists($key)` - sprawdza czy klucz istnieje + - `delete($key)` - usuwa pojedynczy klucz + - `deletePattern($pattern)` - usuwa klucze według wzorca + +#### Klasa S (pomocnicza) +- **Plik:** `autoload/class.S.php` +- **Metody cache:** + - `clear_redis_cache()` - czyści cały cache Redis (flushAll) + - `clear_product_cache(int $product_id)` - czyści cache konkretnego produktu + +### Wzorce kluczy Redis + +#### Produkty +``` +shop\product:{product_id}:{lang_id}:{permutation_hash} +``` +- Przechowuje zserializowany obiekt produktu +- TTL: 24 godziny (86400 sekund) +- Klasa: `shop\Product::getFromCache()` - `autoload/shop/class.Product.php:121` + +#### Opcje ilościowe produktu +``` +\shop\Product::get_product_permutation_quantity_options:{product_id}:{permutation} +``` +- Przechowuje informacje o ilości i komunikatach magazynowych +- Klasa: `shop\Product::get_product_permutation_quantity_options()` - `autoload/shop/class.Product.php:549` + +#### Zestawy produktów +``` +\shop\Product::product_sets_when_add_to_basket:{product_id} +``` +- Przechowuje produkty często kupowane razem +- Klasa: `shop\Product::product_sets_when_add_to_basket()` - `autoload/shop/class.Product.php:316` + +## Integracje z systemami zewnętrznymi (CRON) + +### Plik: `cron.php` + +#### Apilo +- **Aktualizacja pojedynczego produktu:** synchronizacja cen i stanow + - Czestotliwosc: Co 10 minut +- **Synchronizacja cennika:** masowa aktualizacja cen z Apilo + - Czestotliwosc: Co 1 godzine + +**Uwaga:** Integracje Sellasist i Baselinker zostaly usuniete w ver. 0.263. + +## Panel Administratora + +### Routing +- Główny katalog: `admin/` +- Template główny: `admin/templates/site/main-layout.php` +- Kontrolery (nowe): `autoload/admin/Controllers/` +- Kontrolery legacy (fallback): `autoload/admin/controls/` + +### Przycisk "Wyczyść cache" +- **Lokalizacja UI:** `admin/templates/site/main-layout.php:172` +- **JavaScript:** `admin/templates/site/main-layout.php:235-274` +- **Endpoint AJAX:** `/admin/settings/clear_cache_ajax/` +- **Kontroler:** `autoload/admin/Controllers/SettingsController.php:43-60` +- **Działanie:** + 1. Pokazuje spinner "Czyszczę cache..." + 2. Czyści katalogi: `temp/`, `thumbs/` + 3. Wykonuje `flushAll()` na Redis + 4. Pokazuje "Cache wyczyszczony!" przez 2 sekundy + 5. Przywraca stan początkowy + +## Struktura katalogów + +``` +shopPRO/ +├── admin/ # Panel administratora +│ ├── templates/ # Szablony widoków +│ └── layout/ # Zasoby CSS/JS/ikony +├── autoload/ # Klasy autoloadowane +│ ├── admin/ # Klasy panelu admin +│ │ ├── Controllers/ # Nowe kontrolery DI +│ │ ├── controls/ # Kontrolery legacy (fallback) +│ │ └── factory/ # Fabryki/helpery +│ ├── Domain/ # Repozytoria/logika domenowa +│ ├── front/ # Klasy frontendu +│ │ └── factory/ # Fabryki/helpery +│ └── shop/ # Klasy sklepu +├── docs/ # Dokumentacja techniczna +├── libraries/ # Biblioteki zewnętrzne +├── temp/ # Cache tymczasowy +├── thumbs/ # Miniatury zdjęć +└── cron.php # Zadania CRON +``` + +## Baza danych + +### Główne tabele produktów +- `pp_shop_products` - produkty główne +- `pp_shop_products_langs` - tłumaczenia produktów +- `pp_shop_products_images` - zdjęcia produktów +- `pp_shop_products_categories` - kategorie produktów +- `pp_shop_products_custom_fields` - pola własne produktów + +### Tabele integracji +- Kolumny w `pp_shop_products`: + - `apilo_product_id`, `apilo_product_name`, `apilo_get_data_date` +- Tabele ustawien: + - `pp_shop_apilo_settings` (key-value) + - `pp_shop_shoppro_settings` (key-value) + +Pelna dokumentacja tabel: `DATABASE_STRUCTURE.md` + +## Konfiguracja + +### Redis +- Konfiguracja: `config.php` (zmienna `$config['redis']`) +- Parametry: host, port, password + +### Autoload +- Funkcja: `__autoload_my_classes()` w `cron.php:6` +- Wzorzec: `autoload/{namespace}/class.{ClassName}.php` + +## Klasy pomocnicze + +### \S (autoload/class.S.php) +Główna klasa helper z metodami: +- `seo($val)` - generowanie URL SEO +- `normalize_decimal($val, $precision)` - normalizacja liczb +- `send_email()` - wysyłanie emaili +- `delete_dir($dir)` - usuwanie katalogów +- `htacces()` - generowanie .htaccess i sitemap.xml + +### Medoo +- Plik: `libraries/medoo/medoo.php` +- Zmienna: `$mdb` +- ORM do operacji na bazie danych + +## Najważniejsze wzorce + +### Namespace'y +- `\admin\Controllers\` - nowe kontrolery panelu admin (DI) +- `\admin\controls\` - kontrolery legacy (fallback) +- `\Domain\` - repozytoria/logika domenowa +- `\admin\factory\` - helpery/fabryki admin +- `\front\factory\` - helpery/fabryki frontend +- `\shop\` - klasy sklepu (Product, Order, itp.) + +### Cachowanie produktów +```php +// Pobranie produktu z cache +$product = \shop\Product::getFromCache($product_id, $lang_id, $permutation_hash); + +// Czyszczenie cache produktu +\S::clear_product_cache($product_id); + +// Czyszczenie całego cache +\S::clear_redis_cache(); +``` + +## Refaktoryzacja do Domain-Driven Architecture + +### Nowa struktura (w trakcie migracji) +``` +autoload/ +├── Domain/ # Nowa warstwa biznesowa (namespace \Domain\) +│ ├── Product/ +│ │ └── ProductRepository.php +│ ├── Banner/ +│ │ └── BannerRepository.php +│ ├── Settings/ +│ │ └── SettingsRepository.php +│ ├── Cache/ +│ │ └── CacheRepository.php +│ ├── Article/ +│ │ └── ArticleRepository.php +│ ├── User/ +│ │ └── UserRepository.php +│ ├── Languages/ +│ │ └── LanguagesRepository.php +│ ├── Layouts/ +│ │ └── LayoutsRepository.php +│ ├── Newsletter/ +│ │ └── NewsletterRepository.php +│ ├── Scontainers/ +│ │ └── ScontainersRepository.php +│ ├── Dictionaries/ +│ │ └── DictionariesRepository.php +│ ├── Pages/ +│ │ └── PagesRepository.php +│ ├── Integrations/ +│ │ └── IntegrationsRepository.php +│ ├── Promotion/ +│ │ └── PromotionRepository.php +│ ├── Coupon/ +│ │ └── CouponRepository.php +│ ├── ShopStatus/ +│ │ └── ShopStatusRepository.php +│ └── ... +├── admin/ +│ ├── Controllers/ # Nowe kontrolery (namespace \admin\Controllers\) +│ ├── class.Site.php # Router: nowy kontroler → fallback stary +│ ├── controls/ # Stare kontrolery (niezależny fallback) +│ ├── factory/ # Stare helpery (niezależny fallback) +│ └── view/ # Widoki (statyczne - bez zmian) +├── shop/ # Legacy - fasady do Domain +└── front/factory/ # Legacy - stopniowo migrowane +``` + +### Routing admin (admin\Site::route()) +1. Sprawdź mapę `$newControllers` → utwórz instancję z DI → wywołaj +2. Jeśli nowy kontroler nie istnieje (`class_exists()` = false) → fallback na `admin\controls\` +3. Stary kontroler jest NIEZALEŻNY od nowych klas (bezpieczny fallback) + +### Dependency Injection +Nowe klasy używają **Dependency Injection** zamiast `global` variables: +```php +// STARE +global $mdb; +$quantity = $mdb->get('pp_shop_products', 'quantity', ['id' => $id]); + +// NOWE +$repository = new \Domain\Product\ProductRepository($mdb); +$quantity = $repository->getQuantity($id); +``` + +## Testowanie (tylko dla deweloperów) + +**UWAGA:** Pliki testów NIE są częścią aktualizacji dla klientów! + +### Narzędzia +- **PHPUnit 9.6.34** - framework testowy +- **test.bat** - uruchamianie testów +- **composer.json** - autoloading PSR-4 + +Pelna dokumentacja testow: `TESTING.md` + +--- +*Dokument aktualizowany: 2026-02-14* diff --git a/docs/REFACTORING_PLAN.md b/docs/REFACTORING_PLAN.md new file mode 100644 index 0000000..7cceb37 --- /dev/null +++ b/docs/REFACTORING_PLAN.md @@ -0,0 +1,276 @@ +# Plan Refaktoryzacji shopPRO - Domain-Driven Architecture + +## Cel +Stopniowe przeniesienie logiki biznesowej do architektury warstwowej: +- **Domain/** - logika biznesowa (core) +- **Admin/** - warstwa administratora +- **Frontend/** - warstwa użytkownika +- **Shared/** - współdzielone narzędzia + +## Docelowa struktura + +``` +autoload/ +├── Domain/ # Logika biznesowa (CORE) - namespace \Domain\ +│ ├── Product/ +│ │ ├── ProductRepository.php +│ │ ├── ProductService.php # (przyszłość) +│ │ └── ProductCacheService.php # (przyszłość) +│ ├── Banner/ +│ │ └── BannerRepository.php +│ ├── Settings/ +│ │ └── SettingsRepository.php +│ ├── Cache/ +│ │ └── CacheRepository.php +│ ├── Order/ +│ ├── Category/ +│ └── ... +│ +├── admin/ # Warstwa administratora (istniejący katalog!) +│ ├── Controllers/ # Nowe kontrolery - namespace \admin\Controllers\ +│ ├── controls/ # Stare kontrolery (legacy fallback) +│ ├── factory/ # Stare helpery (legacy) +│ └── view/ # Widoki (statyczne - OK bez zmian) +│ +├── Frontend/ # Warstwa użytkownika (przyszłość) +│ ├── Controllers/ +│ └── Services/ +│ +├── Shared/ # Współdzielone narzędzia +│ ├── Cache/ +│ │ ├── CacheHandler.php +│ │ └── RedisConnection.php +│ └── Helpers/ +│ └── S.php +│ +└── [LEGACY] # Stare klasy (stopniowo deprecated) + ├── shop/ + ├── admin/factory/ + └── front/factory/ +``` + +### WAŻNE: Konwencja namespace → katalog (Linux case-sensitive!) +- `\Domain\` → `autoload/Domain/` (duże D - nowy katalog) +- `\admin\Controllers\` → `autoload/admin/Controllers/` (małe a - istniejący katalog) +- NIE używać `\Admin\` (duże A) bo na serwerze Linux katalog to `admin/` (małe a) + +## Zasady migracji + +### 1. Stopniowość +- Przenosimy **jedną funkcję na raz** +- Zachowujemy kompatybilność wsteczną +- Stare klasy działają jako fasady do nowych + +### 2. Dependency Injection zamiast statycznych metod +```php +// ❌ STARE - statyczne +class Product { + public static function getQuantity($id) { + global $mdb; + return $mdb->get('pp_shop_products', 'quantity', ['id' => $id]); + } +} + +// ✅ NOWE - instancje z DI +class ProductRepository { + private $db; + + public function __construct($db) { + $this->db = $db; + } + + public function getQuantity($id) { + return $this->db->get('pp_shop_products', 'quantity', ['id' => $id]); + } +} +``` + +### 3. Fasady dla kompatybilności +```php +// Stara klasa wywołuje nową +namespace shop; + +class Product { + public static function getQuantity($id) { + global $mdb; + $repo = new \Domain\Product\ProductRepository($mdb); + return $repo->getQuantity($id); + } +} +``` + +## Proces migracji funkcji + +### Krok 1: Wybór funkcji +- Wybierz prostą funkcję statyczną +- Sprawdź jej zależności +- Przeanalizuj gdzie jest używana + +### Krok 2: Stworzenie nowej struktury +- Utwórz folder `Domain/{Module}/` +- Stwórz odpowiednią klasę (Repository/Service/Entity) +- Przenieś logikę + +### Krok 3: Znalezienie użyć +```bash +grep -r "Product::getQuantity" . +``` + +### Krok 4: Aktualizacja wywołań +- Opcja A: Bezpośrednie wywołanie nowej klasy +- Opcja B: Fasada w starej klasie (zalecane na początek) + +### Krok 5: Testy +- Napisz test jednostkowy dla nowej funkcji +- Sprawdź czy stare wywołania działają + +## Status migracji + +### ✅ Zmigrowane moduły +| # | Modul | Wersja | Zakres | +|---|-------|--------|--------| +| 1 | Cache | 0.237 | CacheHandler, RedisConnection, clear_product_cache | +| 2 | Product | 0.238-0.252 | getQuantity, getPrice, getName, archive/unarchive | +| 3 | Banner | 0.239 | find, delete, save, kontroler DI | +| 4 | Settings | 0.240/0.250 | saveSettings, getSettings, kontroler DI | +| 5 | Dictionaries | 0.251 | listForAdmin, find, save, delete, kontroler DI | +| 6 | ProductArchive | 0.252 | kontroler DI, table-list | +| 7 | Filemanager | 0.252 | kontroler DI, fix Invalid Key | +| 8 | Users | 0.253 | CRUD, logon, 2FA, kontroler DI | +| 9 | Languages | 0.254 | languages + translations, kontroler DI | +| 10 | Layouts | 0.256 | find, save, delete, menusWithPages, categoriesTree | +| 11 | Newsletter | 0.257-0.258 | subskrybenci, szablony, ustawienia | +| 12 | Scontainers | 0.259 | listForAdmin, find, save, delete | +| 13 | ArticlesArchive | 0.260 | restore, deletePermanently | +| 14 | Articles | 0.261 | pelna migracja (CRUD, AJAX, galeria, pliki) | +| 15 | Pages | 0.262 | menu/page CRUD, drzewo stron, AJAX | +| 16 | Integrations | 0.263 | Apilo/ShopPRO, cleanup Sellasist/Baselinker | +| 17 | ShopPromotion | 0.264-0.265 | listForAdmin, find, save, delete, categoriesTree | +| 18 | ShopCoupon | 0.266 | listForAdmin, find, save, delete, categoriesTree | +| 19 | ShopStatuses | 0.267 | listForAdmin, find, save, color picker | + +### Product - szczegolowy status +- ✅ getQuantity (ver. 0.238) +- ✅ getPrice (ver. 0.239) +- ✅ getName (ver. 0.239) +- ✅ archive / unarchive (ver. 0.241/0.252) +- [ ] is_product_on_promotion +- [ ] getFromCache +- [ ] getProductImg + +### 📋 Do zrobienia +- Order +- Category +- ShopAttribute +- ShopProduct (factory) + +## Kolejność refaktoryzacji (priorytet) + +1-13: ✅ Cache, Product, Banner, Settings, Dictionaries, ProductArchive, Filemanager, Users, Pages, Integrations, ShopPromotion, ShopCoupon, ShopStatuses + +Nastepne: +14. **Order** +15. **Category** +16. **ShopAttribute** + +## Form Edit System + +Nowy uniwersalny system formularzy edycji: +- ✅ Klasy ViewModel: `FormFieldType`, `FormField`, `FormTab`, `FormAction`, `FormEditViewModel` +- ✅ Walidacja: `FormValidator` z obsługą reguł per pole i sekcje językowe +- ✅ Persist: `FormRequestHandler` - zapamiętywanie danych przy błędzie walidacji +- ✅ Renderer: `FormFieldRenderer` - renderowanie wszystkich typów pól +- ✅ Szablon: `admin/templates/components/form-edit.php` - uniwersalny layout +- Wspierane typy pól: text, number, email, password, date, datetime, switch, select, textarea, editor, image, file, hidden, lang_section, color +- Obsługa zakładek (vertical) i sekcji językowych (horizontal) +- **Do zrobienia**: Przerobić pozostałe kontrolery/formularze (Product, Category, Pages, itd.) + +Pelna dokumentacja: `docs/FORM_EDIT_SYSTEM.md` + +## Zasady kodu + +### 1. SOLID Principles +- **S**ingle Responsibility - jedna klasa = jedna odpowiedzialność +- **O**pen/Closed - otwarty na rozszerzenia, zamknięty na modyfikacje +- **L**iskov Substitution - podklasy mogą zastąpić nadklasy +- **I**nterface Segregation - wiele małych interfejsów +- **D**ependency Inversion - zależności od abstrakcji + +### 2. Nazewnictwo +- **Entity** - `Product.php` (reprezentuje obiekt domenowy) +- **Repository** - `ProductRepository.php` (dostęp do danych) +- **Service** - `ProductService.php` (logika biznesowa) +- **Controller** - `ProductController.php` (obsługa requestów) + +### 3. Type Hinting +```php +// ✅ DOBRE +public function getQuantity(int $id): ?int { + return $this->db->get('pp_shop_products', 'quantity', ['id' => $id]); +} + +// ❌ ZŁE +public function getQuantity($id) { + return $this->db->get('pp_shop_products', 'quantity', ['id' => $id]); +} +``` + +## Narzędzia pomocnicze + +### Autoloader (produkcja) +Autoloader w 9 entry pointach obsługuje dwie konwencje: +1. `autoload/{namespace}/class.{ClassName}.php` (legacy) +2. `autoload/{namespace}/{ClassName}.php` (PSR-4, fallback) + +Entry pointy: `index.php`, `ajax.php`, `api.php`, `cron.php`, `cron-turstmate.php`, `download.php`, `admin/index.php`, `admin/ajax.php`, `cron/cron-xml.php` + +### Static Analysis +```bash +composer require --dev phpstan/phpstan +vendor/bin/phpstan analyse autoload/Domain +``` + +## Testowanie + +### Framework: PHPUnit +```bash +composer test +``` + +### Struktura testów +``` +tests/ +├── Unit/ +│ ├── Domain/ +│ │ ├── Article/ArticleRepositoryTest.php +│ │ ├── Banner/BannerRepositoryTest.php +│ │ ├── Cache/CacheRepositoryTest.php +│ │ ├── Coupon/CouponRepositoryTest.php +│ │ ├── Dictionaries/DictionariesRepositoryTest.php +│ │ ├── Integrations/IntegrationsRepositoryTest.php +│ │ ├── Product/ProductRepositoryTest.php +│ │ ├── Promotion/PromotionRepositoryTest.php +│ │ ├── Settings/SettingsRepositoryTest.php +│ │ ├── ShopStatus/ShopStatusRepositoryTest.php +│ │ └── User/UserRepositoryTest.php +│ └── admin/ +│ └── Controllers/ +│ ├── ArticlesControllerTest.php +│ ├── DictionariesControllerTest.php +│ ├── IntegrationsControllerTest.php +│ ├── ProductArchiveControllerTest.php +│ ├── SettingsControllerTest.php +│ ├── ShopCouponControllerTest.php +│ ├── ShopPromotionControllerTest.php +│ ├── ShopStatusesControllerTest.php +│ └── UsersControllerTest.php +└── Integration/ +``` +**Łącznie: 254 testów, 736 asercji** + +Pelna dokumentacja testow: `TESTING.md` + +--- +*Rozpoczęto: 2025-02-05* +*Ostatnia aktualizacja: 2026-02-14* +*Changelog zmian: `docs/CHANGELOG.md`* diff --git a/TESTING.md b/docs/TESTING.md similarity index 90% rename from TESTING.md rename to docs/TESTING.md index d7675b0..00d3c4c 100644 --- a/TESTING.md +++ b/docs/TESTING.md @@ -36,7 +36,7 @@ Alternatywnie (Git Bash): Ostatnio zweryfikowano: 2026-02-13 ```text -OK (235 tests, 682 assertions) +OK (254 tests, 736 assertions) ``` ## Struktura testow @@ -51,10 +51,12 @@ tests/ | | |-- Cache/CacheRepositoryTest.php | | |-- Coupon/CouponRepositoryTest.php | | |-- Dictionaries/DictionariesRepositoryTest.php +| | |-- Integrations/IntegrationsRepositoryTest.php | | |-- Product/ProductRepositoryTest.php +| | |-- Promotion/PromotionRepositoryTest.php | | |-- Settings/SettingsRepositoryTest.php -| | |-- User/UserRepositoryTest.php -| | `-- Integrations/IntegrationsRepositoryTest.php +| | |-- ShopStatus/ShopStatusRepositoryTest.php +| | `-- User/UserRepositoryTest.php | `-- admin/ | `-- Controllers/ | |-- ArticlesControllerTest.php @@ -63,6 +65,8 @@ tests/ | |-- ProductArchiveControllerTest.php | |-- SettingsControllerTest.php | |-- ShopCouponControllerTest.php +| |-- ShopPromotionControllerTest.php +| |-- ShopStatusesControllerTest.php | `-- UsersControllerTest.php `-- Integration/ ``` @@ -334,3 +338,14 @@ Nowe testy dodane 2026-02-13: Ponowna weryfikacja po poprawkach UI (drzewko + checkboxy): 2026-02-13 - `OK (235 tests, 682 assertions)` + +## Aktualizacja suite (ShopStatuses refactor, ver. 0.267) +Ostatnio zweryfikowano: 2026-02-14 + +```text +OK (254 tests, 736 assertions) +``` + +Nowe testy dodane 2026-02-14: +- `tests/Unit/Domain/ShopStatus/ShopStatusRepositoryTest.php` (9 testow: find z ID=0, find null apilo, save update, save z ID=0, empty apilo sets null, reject negative ID, getApiloStatusId, getByIntegrationStatusId, allStatuses, whitelist sortowania) +- `tests/Unit/admin/Controllers/ShopStatusesControllerTest.php` (5 testow: kontrakty metod, brak aliasow legacy, return types, DI konstruktora) diff --git a/UPDATE_INSTRUCTIONS.md b/docs/UPDATE_INSTRUCTIONS.md similarity index 100% rename from UPDATE_INSTRUCTIONS.md rename to docs/UPDATE_INSTRUCTIONS.md diff --git a/tests/Unit/Domain/ShopStatus/ShopStatusRepositoryTest.php b/tests/Unit/Domain/ShopStatus/ShopStatusRepositoryTest.php new file mode 100644 index 0000000..f921c60 --- /dev/null +++ b/tests/Unit/Domain/ShopStatus/ShopStatusRepositoryTest.php @@ -0,0 +1,253 @@ +createMock(\medoo::class); + $mockDb->expects($this->never())->method('get'); + + $repository = new ShopStatusRepository($mockDb); + $this->assertNull($repository->find(-1)); + } + + public function testFindReturnsNullWhenNotFound(): void + { + $mockDb = $this->createMock(\medoo::class); + $mockDb->expects($this->once()) + ->method('get') + ->with('pp_shop_statuses', '*', ['id' => 99]) + ->willReturn(null); + + $repository = new ShopStatusRepository($mockDb); + $this->assertNull($repository->find(99)); + } + + public function testFindReturnsStatusWithIdZero(): void + { + $mockDb = $this->createMock(\medoo::class); + $mockDb->expects($this->once()) + ->method('get') + ->with('pp_shop_statuses', '*', ['id' => 0]) + ->willReturn([ + 'id' => '0', + 'status' => 'zamówienie złożone', + 'color' => '#ff0000', + 'o' => '0', + 'apilo_status_id' => '5', + ]); + + $repository = new ShopStatusRepository($mockDb); + $result = $repository->find(0); + + $this->assertIsArray($result); + $this->assertSame(0, $result['id']); + $this->assertSame('zamówienie złożone', $result['status']); + $this->assertSame('#ff0000', $result['color']); + $this->assertSame(0, $result['o']); + $this->assertSame(5, $result['apilo_status_id']); + } + + public function testFindNormalizesNullApiloStatusId(): void + { + $mockDb = $this->createMock(\medoo::class); + $mockDb->expects($this->once()) + ->method('get') + ->with('pp_shop_statuses', '*', ['id' => 1]) + ->willReturn([ + 'id' => '1', + 'status' => 'zamówienie opłacone', + 'color' => '', + 'o' => '1', + 'apilo_status_id' => null, + ]); + + $repository = new ShopStatusRepository($mockDb); + $result = $repository->find(1); + + $this->assertIsArray($result); + $this->assertSame(1, $result['id']); + $this->assertNull($result['apilo_status_id']); + } + + public function testSaveUpdatesColorAndApiloStatusId(): void + { + $mockDb = $this->createMock(\medoo::class); + $updateRow = null; + $updateWhere = null; + + $mockDb->expects($this->once()) + ->method('update') + ->willReturnCallback(function ($table, $row, $where) use (&$updateRow, &$updateWhere) { + $this->assertSame('pp_shop_statuses', $table); + $updateRow = $row; + $updateWhere = $where; + return true; + }); + + $repository = new ShopStatusRepository($mockDb); + $id = $repository->save(3, [ + 'color' => ' #00ff00 ', + 'apilo_status_id' => '12', + ]); + + $this->assertSame(3, $id); + $this->assertSame('#00ff00', $updateRow['color']); + $this->assertSame(12, $updateRow['apilo_status_id']); + $this->assertSame(['id' => 3], $updateWhere); + } + + public function testSaveWithIdZeroWorks(): void + { + $mockDb = $this->createMock(\medoo::class); + $mockDb->expects($this->once()) + ->method('update') + ->with('pp_shop_statuses', $this->anything(), ['id' => 0]); + + $repository = new ShopStatusRepository($mockDb); + $id = $repository->save(0, ['color' => '#aabbcc']); + + $this->assertSame(0, $id); + } + + public function testSaveWithEmptyApiloStatusIdSetsNull(): void + { + $mockDb = $this->createMock(\medoo::class); + $updateRow = null; + + $mockDb->expects($this->once()) + ->method('update') + ->willReturnCallback(function ($table, $row) use (&$updateRow) { + $updateRow = $row; + }); + + $repository = new ShopStatusRepository($mockDb); + $repository->save(1, ['color' => '#000', 'apilo_status_id' => '']); + + $this->assertNull($updateRow['apilo_status_id']); + } + + public function testSaveRejectsNegativeId(): void + { + $mockDb = $this->createMock(\medoo::class); + $mockDb->expects($this->never())->method('update'); + + $repository = new ShopStatusRepository($mockDb); + $this->assertSame(0, $repository->save(-1, ['color' => '#fff'])); + } + + public function testGetApiloStatusIdReturnsValue(): void + { + $mockDb = $this->createMock(\medoo::class); + $mockDb->expects($this->once()) + ->method('get') + ->with('pp_shop_statuses', 'apilo_status_id', ['id' => 4]) + ->willReturn(15); + + $repository = new ShopStatusRepository($mockDb); + $this->assertSame(15, $repository->getApiloStatusId(4)); + } + + public function testGetApiloStatusIdReturnsNullWhenNotSet(): void + { + $mockDb = $this->createMock(\medoo::class); + $mockDb->expects($this->once()) + ->method('get') + ->willReturn(null); + + $repository = new ShopStatusRepository($mockDb); + $this->assertNull($repository->getApiloStatusId(4)); + } + + public function testGetByIntegrationStatusIdForApilo(): void + { + $mockDb = $this->createMock(\medoo::class); + $mockDb->expects($this->once()) + ->method('get') + ->with('pp_shop_statuses', 'id', ['apilo_status_id' => 15]) + ->willReturn(4); + + $repository = new ShopStatusRepository($mockDb); + $this->assertSame(4, $repository->getByIntegrationStatusId('apilo', 15)); + } + + public function testGetByIntegrationStatusIdReturnsNullForUnknownIntegration(): void + { + $mockDb = $this->createMock(\medoo::class); + $mockDb->expects($this->never())->method('get'); + + $repository = new ShopStatusRepository($mockDb); + $this->assertNull($repository->getByIntegrationStatusId('unknown', 1)); + } + + public function testAllStatusesReturnsOrderedList(): void + { + $mockDb = $this->createMock(\medoo::class); + $mockDb->expects($this->once()) + ->method('select') + ->with('pp_shop_statuses', ['id', 'status'], ['ORDER' => ['o' => 'ASC']]) + ->willReturn([ + ['id' => 0, 'status' => 'złożone'], + ['id' => 1, 'status' => 'opłacone'], + ]); + + $repository = new ShopStatusRepository($mockDb); + $result = $repository->allStatuses(); + + $this->assertSame([0 => 'złożone', 1 => 'opłacone'], $result); + } + + public function testListForAdminWhitelistsSortAndDirection(): void + { + $mockDb = $this->createMock(\medoo::class); + $queries = []; + + $mockDb->method('query') + ->willReturnCallback(function ($sql, $params = []) use (&$queries) { + $queries[] = ['sql' => $sql, 'params' => $params]; + + if (strpos($sql, 'COUNT(0)') !== false) { + return new class { + public function fetchAll() + { + return [[2]]; + } + }; + } + + return new class { + public function fetchAll() + { + return [[ + 'id' => 0, + 'status' => 'złożone', + 'color' => '#fff', + 'o' => 0, + 'apilo_status_id' => null, + ]]; + } + }; + }); + + $repository = new ShopStatusRepository($mockDb); + $result = $repository->listForAdmin( + [], + 'status DESC; DROP TABLE pp_shop_statuses; --', + 'DESC; DELETE FROM pp_users; --', + 1, + 999 + ); + + $this->assertCount(2, $queries); + $dataSql = $queries[1]['sql']; + + $this->assertMatchesRegularExpression('/ORDER BY\s+ss\.o\s+ASC,\s+ss\.id\s+ASC/i', $dataSql); + $this->assertStringNotContainsString('DROP TABLE', $dataSql); + $this->assertStringNotContainsString('DELETE FROM pp_users', $dataSql); + $this->assertMatchesRegularExpression('/LIMIT\s+100\s+OFFSET\s+0/i', $dataSql); + } +} diff --git a/tests/Unit/admin/Controllers/ShopStatusesControllerTest.php b/tests/Unit/admin/Controllers/ShopStatusesControllerTest.php new file mode 100644 index 0000000..46adbea --- /dev/null +++ b/tests/Unit/admin/Controllers/ShopStatusesControllerTest.php @@ -0,0 +1,57 @@ +repository = $this->createMock(ShopStatusRepository::class); + $this->controller = new ShopStatusesController($this->repository); + } + + public function testConstructorAcceptsRepository(): void + { + $controller = new ShopStatusesController($this->repository); + $this->assertInstanceOf(ShopStatusesController::class, $controller); + } + + public function testHasMainActionMethods(): void + { + $this->assertTrue(method_exists($this->controller, 'list')); + $this->assertTrue(method_exists($this->controller, 'edit')); + $this->assertTrue(method_exists($this->controller, 'save')); + } + + public function testHasNoLegacyAliasMethods(): void + { + $this->assertFalse(method_exists($this->controller, 'view_list')); + $this->assertFalse(method_exists($this->controller, 'status_edit')); + $this->assertFalse(method_exists($this->controller, 'status_save')); + } + + public function testActionMethodReturnTypes(): void + { + $reflection = new \ReflectionClass($this->controller); + + $this->assertEquals('string', (string)$reflection->getMethod('list')->getReturnType()); + $this->assertEquals('string', (string)$reflection->getMethod('edit')->getReturnType()); + $this->assertEquals('void', (string)$reflection->getMethod('save')->getReturnType()); + } + + public function testConstructorRequiresShopStatusRepository(): void + { + $reflection = new \ReflectionClass(ShopStatusesController::class); + $constructor = $reflection->getConstructor(); + $params = $constructor->getParameters(); + + $this->assertCount(1, $params); + $this->assertEquals('Domain\ShopStatus\ShopStatusRepository', $params[0]->getType()->getName()); + } +} diff --git a/updates/0.20/ver_0.268.zip b/updates/0.20/ver_0.268.zip new file mode 100644 index 0000000000000000000000000000000000000000..d28bfab1b291ca784d0406fc3594dc7e69b93923 GIT binary patch literal 25695 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. 0.268 - 14.02.2026
    +- NEW - migracja modulu `ShopStatuses` do architektury Domain + DI (`Domain\ShopStatus\ShopStatusRepository`, `admin\Controllers\ShopStatusesController`) +- UPDATE - modul `/admin/shop_statuses/*` przepiety z legacy `grid/gridEdit` na `components/table-list` i `components/form-edit` +- NEW - nowy typ pola formularza `color` (HTML5 color picker + pole tekstowe zsynchronizowane) +- UPDATE - `front\factory\ShopStatuses` dziala jako fasada do `Domain\ShopStatus\ShopStatusRepository` +- UPDATE - menu admin przepiete na kanoniczny URL `/admin/shop_statuses/list/` +- CLEANUP - usuniete legacy klasy: `autoload/admin/controls/class.ShopStatuses.php`, `autoload/admin/factory/class.ShopStatuses.php` +- UPDATE - reorganizacja dokumentacji technicznej: pliki przeniesione do folderu `docs/` i rozbite na mniejsze pliki tematyczne +- UPDATE - testy: `OK (254 tests, 736 assertions)` + nowe pliki testowe `ShopStatusRepositoryTest`, `ShopStatusesControllerTest` +- UPDATE - pliki aktualizacji: `updates/0.20/ver_0.268.zip`, `ver_0.268_files.txt` +
    ver. 0.267 - 13.02.2026
    - FIX - front: poprawione dobieranie layoutu dla kategorii/produktu/koszyka i innych stron modułowych (fallback do layoutu domyślnego) - FIX - produkt/koszyk: poprawiona obsługa ilości dla kombinacji (stan 0 po dodaniu do koszyka, limit max, odczyt `stock_0_buy`) diff --git a/updates/versions.php b/updates/versions.php index 4a343b3..02913c7 100644 --- a/updates/versions.php +++ b/updates/versions.php @@ -1,5 +1,5 @@