refactor(shop-statuses): migrate to DI, restructure docs into docs/ folder (0.268)

- Migrate ShopStatuses module to Domain + DI architecture
- Add ShopStatusRepository, ShopStatusesController with color picker
- Convert front\factory\ShopStatuses to facade
- Add FormFieldType::COLOR with HTML5 color picker
- Move documentation files to docs/ folder (PROJECT_STRUCTURE, REFACTORING_PLAN, CHANGELOG, FORM_EDIT_SYSTEM, TESTING, DATABASE_STRUCTURE)
- Tests: 254 tests, 736 assertions

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-14 10:43:31 +01:00
parent 7574785d68
commit 847fdbbf3f
29 changed files with 1895 additions and 1572 deletions

View File

@@ -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.

View File

@@ -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<64> users dzia<69>a na `Domain\\User\\UserRepository` + `admin\\Controllers\\UsersController`.
- Usuni<6E>to legacy klasy: `autoload/admin/controls/class.Users.php`, `autoload/admin/factory/class.Users.php`, `autoload/admin/view/class.Users.php`.
- Walidacja: przy w<><77>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 <?php (kompatybilnosc serwerow z short_open_tag=Off).
- Testy: 130 tests, 303 assertions
## Aktualizacja 2026-02-12 (ver. 0.256)
- NOWE: `Domain\\Layouts\\LayoutsRepository` (find/save/delete/listForAdmin, menusWithPages, categoriesTree).
- NOWE: `admin\\Controllers\\LayoutsController` (DI) dla akcji `list`, `layout_edit`, `layout_save`, `layout_delete`.
- UPDATE: `/admin/layouts/view_list/` przepiete z legacy `grid` na `components/table-list` (`PaginatedTableViewModel` + `TableListRequestFactory`).
- UPDATE: `admin/templates/layouts/layout-edit.php` korzysta z danych przekazywanych z repozytorium (bez wywolan `admin\\factory\\Pages` i `admin\\factory\\ShopCategory` w widoku).
- NOWE: `admin/templates/layouts/subcategories-list.php` (rekurencyjny partial bez zaleznosci od legacy factory).
- UPDATE: `Domain\\Languages\\LanguagesRepository` ma wspolna metode `defaultLanguageId()` wykorzystywana m.in. w `LayoutsController`.
- CLEANUP: usuniete legacy klasy `autoload/admin/controls/class.Layouts.php`, `autoload/admin/view/class.Layouts.php`; `admin/factory/class.Layouts.php` dziala jako fasada do `Domain\\Layouts\\LayoutsRepository`.
- UPDATE: `admin\\Controllers\\ArticlesController` pobiera layouty przez `Domain\\Layouts\\LayoutsRepository` (DI).
- Testy: 141 tests, 336 assertions
## Aktualizacja 2026-02-12 (ver. 0.257)
- NOWE: `Domain\\Newsletter\\NewsletterRepository` (subskrybenci, szablony, ustawienia, kolejka wysylki).
- NOWE: `Domain\\Newsletter\\NewsletterPreviewRenderer` (render podgladu newslettera).
- NOWE: `admin\\Controllers\\NewsletterController` (DI) dla akcji `emails_list`, `prepare`, `send`, `preview`, `settings*`, `email_template*`.
- UPDATE: `/admin/newsletter/*` przepiete z legacy `grid/gridEdit` na `components/table-list` i `components/form-edit`.
- UPDATE: nowy endpoint podgladu `/admin/newsletter/preview/` (bez `admin/ajax.php`).
- UPDATE: `admin\\Site` ma fabryke DI dla modulu `Newsletter`.
- UPDATE: `admin\\factory\\Newsletter` dziala jako fasada do `Domain\\Newsletter\\NewsletterRepository`.
- UPDATE: `front\\factory\\Newsletter` nie korzysta z `admin\\view\\Newsletter`.
- CLEANUP: usuniete legacy klasy `autoload/admin/controls/class.Newsletter.php`, `autoload/admin/view/class.Newsletter.php`.
- Testy: 150 tests, 372 assertions
## Aktualizacja 2026-02-12 (ver. 0.258)
- UPDATE: modul `/admin/newsletter/` - tymczasowo wylaczono akcje `prepare/send/preview` (Wysylka - przygotowanie).
- UPDATE: modul `/admin/newsletter/` - tymczasowo wylaczono liste `email_templates_user` (Szablony uzytkownika).
- UPDATE: lista i edycja szablonow newslettera w panelu ograniczona do szablonow administracyjnych (`is_admin = 1`).
- CLEANUP: usuniete nieuzywane widoki: `admin/templates/newsletter/prepare.php`, `admin/templates/newsletter/preview.php`, `admin/templates/newsletter/email-templates-user.php`.
## Aktualizacja 2026-02-12 (ver. 0.259)
- NOWE: `Domain\Scontainers\ScontainersRepository` (listForAdmin/find/save/delete/detailsForLanguage + czyszczenie cache frontu).
- NOWE: `admin\Controllers\ScontainersController` (DI) dla akcji `list/view_list`, `container_edit`, `container_save`, `container_delete`.
- UPDATE: `/admin/scontainers/*` przepiete z legacy `grid/gridEdit` na `components/table-list` i `components/form-edit`.
- UPDATE: `admin\Site` ma fabryke DI dla modulu `Scontainers` oraz mapowanie akcji `container_*` -> `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)**.

View File

@@ -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<69>w/zdj<64><6A>
- 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<64>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<74>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 <?php (zgodnosc z short_open_tag=Off).
- Testy po zmianie: 130 tests, 303 assertions.
## Aktualizacja 2026-02-12 (ver. 0.256)
- **Layouts** - **ZMIGROWANE** (2026-02-12) ??
- 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 dostarczonych przez repozytorium (bez wywolan legacy factory)
- NOWE: partial `admin/templates/layouts/subcategories-list.php`
- CLEANUP: usuniete `autoload/admin/controls/class.Layouts.php` i `autoload/admin/view/class.Layouts.php`
- KOMPATYBILNOSC: `autoload/admin/factory/class.Layouts.php` deleguje do repozytorium
- **Languages**
- UPDATE: `Domain\\Languages\\LanguagesRepository::defaultLanguageId()` jako wspolna metoda do pobierania jezyka domyslnego
- **Articles**
- UPDATE: `admin\\Controllers\\ArticlesController` korzysta z `Domain\\Layouts\\LayoutsRepository` (DI) dla listy layoutow
- Testy po zmianie: **141 tests, 336 assertions**
## Aktualizacja 2026-02-12 (ver. 0.257)
- **Newsletter** - **ZMIGROWANE** (2026-02-12)
- NOWE: `Domain\\Newsletter\\NewsletterRepository` (listy admin, szablony, ustawienia, kolejka wysylki)
- NOWE: `Domain\\Newsletter\\NewsletterPreviewRenderer` (wspolny render podgladu)
- NOWE: `admin\\Controllers\\NewsletterController` (DI)
- UPDATE: routing DI (`admin\\Site`) rozszerzony o modul `Newsletter`
- UPDATE: widoki `/admin/newsletter/*` migrowane na `components/table-list` i `components/form-edit`
- UPDATE: `admin\\factory\\Newsletter` jako fasada do repozytorium
- UPDATE: `front\\factory\\Newsletter` bez zaleznosci od `admin\\view\\Newsletter`
- CLEANUP: usuniete `autoload/admin/controls/class.Newsletter.php`, `autoload/admin/view/class.Newsletter.php`
- Testy po zmianie: **150 tests, 372 assertions**
## Aktualizacja 2026-02-12 (ver. 0.258)
- **Newsletter**
- 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`.
## Aktualizacja 2026-02-12 (ver. 0.259)
- **Scontainers** - **ZMIGROWANE** (2026-02-12)
- NOWE: `Domain\Scontainers\ScontainersRepository` (listForAdmin, find, save, delete, detailsForLanguage)
- NOWE: `admin\Controllers\ScontainersController` (DI)
- UPDATE: `/admin/scontainers/view_list/` migrowane na `components/table-list`
- UPDATE: `/admin/scontainers/container_edit/` migrowane na `components/form-edit`
- UPDATE: `admin\factory\Scontainers` jako fasada do repozytorium
- UPDATE: `front\factory\Scontainers` korzysta z repozytorium domenowego
- CLEANUP: usuniete `autoload/admin/controls/class.Scontainers.php` i `autoload/admin/view/class.Scontainers.php`
- Testy po zmianie: **158 tests, 397 assertions**
## Aktualizacja 2026-02-12 (ver. 0.260)
- **ArticlesArchive** - **ZMIGROWANE** (2026-02-12)
- 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`
- UPDATE: routing DI (`admin\Site`) rozszerzony o modul `ArticlesArchive` + mapowanie `article_restore -> 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)**.

View File

@@ -1,79 +1 @@
<script type="text/javascript" src="/libraries/framework/vendor/plugins/ckeditor/ckeditor.js"></script>
<script type="text/javascript" src="/libraries/framework/vendor/plugins/ckeditor/adapters/jquery.js"></script>
<?
global $db;
$apilo_status_types_list[''] = '--- wybierz status apilo.com ---';
foreach ( $this -> apilo_order_status_list as $apilo_status )
{
$apilo_status_types_list[ $apilo_status['id'] ] = $apilo_status['name'];
}
ob_start();
?>
<div id="settings-tabs">
<ul class="resp-tabs-list settings-tabs">
<li><i class="fa fa-file"></i>Ogólne</li>
</ul>
<div class="resp-tabs-container settings-tabs">
<div>
<?= \Html::input( [
'label' => 'Status',
'class' => 'require',
'name' => 'name',
'id' => 'name',
'readonly' => true,
'value' => $this -> status['status']
] );?>
<?= \Html::input( [
'label' => 'Kolor',
'name' => 'color',
'id' => 'color',
'value' => $this -> status['color']
] );?>
<?= \Html::select( [
'label' => 'Status z Apilo',
'name' => 'apilo_status_id',
'id' => 'apilo_status_id',
'values' => $apilo_status_types_list,
'value' => $this -> status['apilo_status_id']
] );?>
</div>
</div>
</div>
<?
$out = ob_get_clean();
$grid = new \gridEdit;
$grid -> 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();
?>
<script type="text/javascript">
$( function()
{
disable_menu();
$( '#settings-tabs' ).easyResponsiveTabs({
width: 'auto',
fit: true,
tabidentify: 'settings-tabs',
type: 'vertical'
});
});
</script>
<?= \Tpl::view('components/form-edit', ['form' => $this->form]); ?>

View File

@@ -1,38 +1 @@
<?php
global $gdb;
foreach ( $this -> 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();
<?= \Tpl::view('components/table-list', ['list' => $this->viewModel]); ?>

View File

@@ -73,7 +73,7 @@
<li><a href="/admin/shop_transport/view_list/"><img src="/admin/layout/icon/icon-menu/bus.svg">Rodzaje transportu</a></li>
<li><a href="/admin/shop_payment_method/view_list/"><img src="/admin/layout/icon/icon-menu/coins-fill.svg">Metody p&#322;atno&#347;ci</a></li>
<li>
<a href="/admin/shop_statuses/view_list/"><i class="fa fa-bars"></i>Statusy zam&#243;wie&#324;</a>
<a href="/admin/shop_statuses/list/"><i class="fa fa-bars"></i>Statusy zam&#243;wie&#324;</a>
</li>
<li><a href="/admin/shop_coupon/list/"><img src="/admin/layout/icon/icon-menu/piggy-bank-coins.svg">Kody rabatowe</a></li>
<li><a href="/admin/shop_promotion/list/"><img src="/admin/layout/icon/icon-menu/burst-sale.svg">Promocje</a></li>

View File

@@ -0,0 +1,181 @@
<?php
namespace Domain\ShopStatus;
class ShopStatusRepository
{
private const MAX_PER_PAGE = 100;
private $db;
public function __construct($db)
{
$this->db = $db;
}
/**
* @return array{items: array<int, array<string, mixed>>, 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;
}
}

View File

@@ -0,0 +1,260 @@
<?php
namespace admin\Controllers;
use Domain\ShopStatus\ShopStatusRepository;
use admin\ViewModels\Common\PaginatedTableViewModel;
use admin\ViewModels\Forms\FormAction;
use admin\ViewModels\Forms\FormEditViewModel;
use admin\ViewModels\Forms\FormField;
use admin\ViewModels\Forms\FormTab;
class ShopStatusesController
{
private ShopStatusRepository $repository;
public function __construct(ShopStatusRepository $repository)
{
$this->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 !== ''
? '<span style="display:inline-block;width:20px;height:20px;background:' . htmlspecialchars($color, ENT_QUOTES, 'UTF-8') . ';border:1px solid #ccc;vertical-align:middle;margin-right:5px;"></span> ' . htmlspecialchars($color, ENT_QUOTES, 'UTF-8')
: '-';
$rows[] = [
'lp' => $lp++ . '.',
'status' => '<a href="/admin/shop_statuses/edit/id=' . $id . '">' . htmlspecialchars($statusName, ENT_QUOTES, 'UTF-8') . '</a>',
'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;
}
}

View File

@@ -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 = '<div class="form-group row">';
$html .= '<label class="col-lg-4 control-label">' . $label . ':</label>';
$html .= '<div class="col-lg-8">';
$html .= '<div style="display:flex;align-items:center;gap:8px;">';
$html .= '<input type="color" id="' . $fieldId . '_picker" value="' . $colorValue . '" style="width:40px;height:34px;padding:2px;border:1px solid #ccc;cursor:pointer;" />';
$html .= '<input type="text" name="' . $fieldName . '" id="' . $fieldId . '" value="' . $colorValue . '" class="form-control" style="max-width:150px;" />';
$html .= '</div>';
$html .= '</div>';
$html .= '</div>';
$html .= '<script>$(function(){'
. 'var $p=$("#' . $fieldId . '_picker"),$t=$("#' . $fieldId . '");'
. '$p.on("input",function(){$t.val(this.value);});'
. '$t.on("input",function(){var v=this.value;if(/^#[0-9a-fA-F]{6}$/.test(v))$p.val(v);});'
. '});</script>';
return $this->wrapWithError($html, $error);
}
public function renderCustom(FormField $field): string
{
return (string)($field->customHtml ?? '');

View File

@@ -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(

View File

@@ -21,4 +21,5 @@ class FormFieldType
public const HIDDEN = 'hidden';
public const LANG_SECTION = 'lang_section';
public const CUSTOM = 'custom';
public const COLOR = 'color';
}

View File

@@ -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;

View File

@@ -1,34 +0,0 @@
<?
namespace admin\controls;
class ShopStatuses {
// status_save
public static function status_save()
{
$response = [ 'status' => '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' ) ),
] );
}
}

View File

@@ -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 {

View File

@@ -1,24 +0,0 @@
<?
namespace admin\factory;
class ShopStatuses {
// get_status
public static function get_status( $id )
{
global $mdb;
return $mdb -> 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;
}
}

View File

@@ -1,4 +1,4 @@
<?
<?php
namespace front\factory;
class ShopStatuses {
@@ -6,15 +6,15 @@ class ShopStatuses {
// get_apilo_status_id
static public function get_apilo_status_id( $status_id ) {
global $mdb;
return $mdb -> 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 );
}
}

315
docs/CHANGELOG.md Normal file
View File

@@ -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 `<?php` (short_open_tag=Off)
- Testy: **OK (130 tests, 303 assertions)**
---
## ver. 0.254 (2026-02-12) - Languages
- NOWE: `Domain\Languages\LanguagesRepository` (languages + translations CRUD/list)
- NOWE: `admin\Controllers\LanguagesController` (DI)
- UPDATE: widoki `languages/*` migrowane na `components/table-list` i `components/form-edit`
- CLEANUP: usuniete `controls/Languages`, `view/Languages`; `factory/Languages` jako fasada
- Testy: **OK (130 tests, 301 assertions)**
---
## ver. 0.253 (2026-02-12) - Users
- NOWE: `Domain\User\UserRepository` (CRUD, logon, 2FA, checkLogin)
- NOWE: `admin\Controllers\UsersController` (DI: view_list, user_edit, user_save, user_delete, login_form, twofa)
- UPDATE: `admin\factory\Users` jako fasada; `admin/ajax/users.php` oparty o `UserRepository`
- UPDATE: widoki users migrowane na `components/table-list` i `components/form-edit`
- UPDATE: walidacja warunkowa: `twofa_email` wymagany gdy `twofa_enabled = 1`
- CLEANUP: usuniete `controls/Users`, `factory/Users`, `view/Users`
- Testy: **OK (119 tests, 256 assertions)**
---
## ver. 0.252 (2026-02-10) - ProductArchive + Filemanager
- UPDATE: `ProductArchiveController` i szablony przepiete na `components/table-list`
- UPDATE: CSS/JS dla list wydzielone do osobnych widoków `*-custom-script.php`
- NOWE: `admin\Controllers\FilemanagerController` - przepieto filemanager na nowy routing
- FIX: naprawiono błąd `Invalid Key` w filemanagerze
- CLEANUP: usunieto legacy `controls/Archive`, `controls/Filemanager`, `view/FileManager`, stare szablony
- RENAME: `admin/templates/product_archive/``admin/templates/product-archive/`
- Testy: **OK (82 tests, 181 assertions)**
---
## ver. 0.251 (2026-02-09) - Dictionaries
- NOWE: `Domain\Dictionaries\DictionariesRepository` (listForAdmin, find, save, delete, allUnits)
- NOWE: `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: **OK (82 tests, 181 assertions)**
---
## ver. 0.250 (2026-02-09) - Settings (refaktoryzacja)
- 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: **OK (82 tests, 181 assertions)**
---
## ver. 0.246 (2026-02-07) - Articles legacy cleanup
- CLEANUP: usunięto `autoload/admin/controls/class.Articles.php`
- UPDATE: `admin\Controllers\ArticlesController::galleryOrderSave()` uzywa `ArticleRepository::saveGalleryOrder()`
- FIX: sortowanie list admin po reloadzie - `RewriteRule` dla `/admin/...` ma `QSA`
- FIX: generator `\S::htacces()` komentuje dyrektywy `AddHandler|SetHandler|ForceType`
- Testy: **OK (65 tests, 131 assertions)**
---
## ver. 0.245 (2026-02-06) - Articles::archive
- UPDATE: `Domain\Article\ArticleRepository` - dodano `archive()` (status = -1)
- UPDATE: `admin\Controllers\ArticlesController` - nowa akcja `delete()` z DI
- UPDATE: `admin\factory\Articles::articles_set_archive()` deleguje do repozytorium
- Testy: **OK (59 tests, 123 assertions)**
---
## ver. 0.244 (2026-02-06) - Articles::save
- UPDATE: `Domain\Article\ArticleRepository` - dodano `save()` z prywatnych helperow
- UPDATE: `admin\Controllers\ArticlesController` - nowa akcja `save()` z DI
- UPDATE: `tests/bootstrap.php` - dodano stub `S::seo()`
- Testy: **OK (57 tests, 119 assertions)**
---
## ver. 0.243 (2026-02-06) - Articles cleanup
- UPDATE: `Domain\Article\ArticleRepository` - dodano `deleteNonassignedImages()` i `deleteNonassignedFiles()`
- UPDATE: `admin\Controllers\ArticlesController::edit()` uses repository cleanup methods
- Testy: **OK (50 tests, 95 assertions)**
---
## ver. 0.242 (2026-02-06) - Articles::edit
- NOWE: `Domain\Article\ArticleRepository` (find: artykuł + relacje)
- UPDATE: `admin\Controllers\ArticlesController` - konstruktor DI + `edit()` używa repozytorium
- UPDATE: `admin\factory\Articles::article_details()` deleguje do repozytorium
- Testy: **OK (48 tests, 91 assertions)**
---
## ver. 0.241 (2026-02-06) - ProductArchive
- NOWE: `admin\Controllers\ProductArchiveController` (DI)
- NOWE: `ProductRepository::archive()`, `unarchive()`
- RENAME: `admin/templates/archive/``admin/templates/product_archive/`
- FIX: SQL w `ajax_products_list_archive()` (puste wyszukiwanie + brak `archive = 1`)
- Testy: **OK (50 tests, 95 assertions)**
---
## ver. 0.240 (2026-02-05) - Settings + Cache
- NOWE: `Domain\Settings\SettingsRepository` (fasada → factory)
- NOWE: `Domain\Cache\CacheRepository` (dirs + Redis)
- NOWE: `admin\Controllers\SettingsController` (DI: clearCache, save, view)
- FIX: Brakujący `id="content"` w main-layout.php
- FIX: `persist_edit = true` w settings.php
- Testy: **OK (29 tests, 60 assertions)**
---
## ver. 0.239 (2026-02-05) - Banner + Product
- NOWE: `Domain\Banner\BannerRepository` (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: **OK (15 tests, 31 assertions)**
---
## ver. 0.238 (2025-02-05) - Product Repository
- 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
---
## ver. 0.237 (2025-02-05) - System cache produktów
- 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-14*

View File

@@ -391,3 +391,20 @@ Ustawienia integracji ShopPRO (key-value).
**Uzywane w:** `Domain\Integrations\IntegrationsRepository`, `admin\Controllers\IntegrationsController`, `admin\factory\Integrations`
**Aktualizacja 2026-02-13:** modul `/admin/integrations/` korzysta z `Domain\Integrations\IntegrationsRepository` (DI kontroler + fasada legacy). Usunieto integracje Sellasist i Baselinker.
## pp_shop_statuses
Statusy zamowien sklepu (modul `/admin/shop_statuses`). Statusy sa predefiniowane - brak dodawania/usuwania, mozliwa edycja koloru i mapowania Apilo.
| Kolumna | Opis |
|---------|------|
| id | PK (zaczyna sie od 0!) |
| status | Nazwa statusu (read-only) |
| color | Kolor statusu (hex, np. #ff0000) |
| o | Kolejnosc wyswietlania |
| apilo_status_id | ID statusu w Apilo (NULL gdy brak mapowania) |
| baselinker_status_id | DEPRECATED (usuniety w ver. 0.263) |
| sellasist_status_id | DEPRECATED (usuniety w ver. 0.263) |
**Uzywane w:** `Domain\ShopStatus\ShopStatusRepository`, `admin\Controllers\ShopStatusesController`, `front\factory\ShopStatuses`, `shop\Order`, `cron.php`
**Aktualizacja 2026-02-14 (ver. 0.267):** modul `/admin/shop_statuses` korzysta z `Domain\ShopStatus\ShopStatusRepository` przez `admin\Controllers\ShopStatusesController`. Usunieto legacy klasy `admin\controls\ShopStatuses` i `admin\factory\ShopStatuses`. `front\factory\ShopStatuses` dziala jako fasada do repozytorium.

172
docs/FORM_EDIT_SYSTEM.md Normal file
View File

@@ -0,0 +1,172 @@
# 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) │ │
│ └─────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
```
## Pliki systemu
| Plik | Opis |
|------|------|
| `autoload/admin/ViewModels/Forms/FormFieldType.php` | Stale typow pol |
| `autoload/admin/ViewModels/Forms/FormField.php` | Factory methods per typ |
| `autoload/admin/ViewModels/Forms/FormTab.php` | Zakladki |
| `autoload/admin/ViewModels/Forms/FormAction.php` | Akcje (zapisz, anuluj) |
| `autoload/admin/ViewModels/Forms/FormEditViewModel.php` | ViewModel formularza |
| `autoload/admin/Support/Forms/FormValidator.php` | Walidacja pol |
| `autoload/admin/Support/Forms/FormRequestHandler.php` | Obsluga POST + persist |
| `autoload/admin/Support/Forms/FormFieldRenderer.php` | Renderowanie HTML |
| `admin/templates/components/form-edit.php` | Uniwersalny szablon |
## 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)` | - |
| `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*

254
docs/PROJECT_STRUCTURE.md Normal file
View File

@@ -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*

276
docs/REFACTORING_PLAN.md Normal file
View File

@@ -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`*

View File

@@ -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)

View File

@@ -0,0 +1,253 @@
<?php
namespace Tests\Unit\Domain\ShopStatus;
use PHPUnit\Framework\TestCase;
use Domain\ShopStatus\ShopStatusRepository;
class ShopStatusRepositoryTest extends TestCase
{
public function testFindReturnsNullForNegativeId(): void
{
$mockDb = $this->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);
}
}

View File

@@ -0,0 +1,57 @@
<?php
namespace Tests\Unit\admin\Controllers;
use PHPUnit\Framework\TestCase;
use admin\Controllers\ShopStatusesController;
use Domain\ShopStatus\ShopStatusRepository;
class ShopStatusesControllerTest extends TestCase
{
private $repository;
private $controller;
protected function setUp(): void
{
$this->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());
}
}

BIN
updates/0.20/ver_0.268.zip Normal file

Binary file not shown.

View File

@@ -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

View File

@@ -1,3 +1,14 @@
<b>ver. 0.268 - 14.02.2026</b><br />
- 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`
<hr>
<b>ver. 0.267 - 13.02.2026</b><br />
- 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`)

View File

@@ -1,5 +1,5 @@
<?
$current_ver = 267;
$current_ver = 268;
for ($i = 1; $i <= $current_ver; $i++)
{