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(); -?> -
- -
-
- '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 0000000..d28bfab Binary files /dev/null and b/updates/0.20/ver_0.268.zip differ diff --git a/updates/0.20/ver_0.268_files.txt b/updates/0.20/ver_0.268_files.txt new file mode 100644 index 0000000..917f8ba --- /dev/null +++ b/updates/0.20/ver_0.268_files.txt @@ -0,0 +1,7 @@ +F: ../autoload/admin/controls/class.ShopStatuses.php +F: ../autoload/admin/factory/class.ShopStatuses.php +F: ../PROJECT_STRUCTURE.md +F: ../REFACTORING_PLAN.md +F: ../DATABASE_STRUCTURE.md +F: ../TESTING.md +F: ../UPDATE_INSTRUCTIONS.md diff --git a/updates/changelog.php b/updates/changelog.php index ba595e9..ef3cbf4 100644 --- a/updates/changelog.php +++ b/updates/changelog.php @@ -1,3 +1,14 @@ +ver. 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 @@