diff --git a/AGENTS.md b/AGENTS.md index d77e1ff..0f57855 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -5,6 +5,22 @@ Gdy użytkownik napisze `KONIEC PRACY`, wykonaj kolejno: 1. Przeprowadzenie testów. -2. Przygotowanie aktualizacji (ZIP, plik z usuwanymi plikami, plik SQL jeśli wymagany). -3. Commit. -4. Push. +2. Aktualizacja dokumentacji technicznej, jeśli zmiany tego wymagają: + - `DATABASE_STRUCTURE.md` + - `PROJECT_STRUCTURE.md` + - `REFACTORING_PLAN.md` + - `TESTING.md` +3. Przygotowanie aktualizacji (ZIP, plik z usuwanymi plikami, plik SQL jeśli wymagany). +4. Commit. +5. Push. + +## PRZED ROZPOCZĘCIEM PRACY + +Przed rozpoczęciem implementacji sprawdź aktualną zawartość: + +- `DATABASE_STRUCTURE.md` +- `PROJECT_STRUCTURE.md` +- `REFACTORING_PLAN.md` +- `TESTING.md` + +To ma pomóc zachować spójność zmian i dokumentacji. diff --git a/DATABASE_STRUCTURE.md b/DATABASE_STRUCTURE.md index 462452c..6a7837e 100644 --- a/DATABASE_STRUCTURE.md +++ b/DATABASE_STRUCTURE.md @@ -138,3 +138,24 @@ Pliki artykułów. | src | Ścieżka do pliku | **Używane w:** `Domain\Article\ArticleRepository::find()` + +## pp_units +Jednostki/slowniki (np. jednostki produktu). + +| Kolumna | Opis | +|---------|------| +| id | PK | + +**Używane w:** `Domain\Dictionaries\DictionariesRepository`, `admin\controls\ShopProduct` + +## pp_units_langs +Tlumaczenia jednostek (per jezyk). + +| Kolumna | Opis | +|---------|------| +| id | PK | +| unit_id | FK do pp_units | +| lang_id | ID jezyka (np. 'pl') | +| text | Nazwa jednostki | + +**Używane w:** `Domain\Dictionaries\DictionariesRepository` diff --git a/PROJECT_STRUCTURE.md b/PROJECT_STRUCTURE.md index 83d4ead..dcb303e 100644 --- a/PROJECT_STRUCTURE.md +++ b/PROJECT_STRUCTURE.md @@ -83,7 +83,8 @@ shop\product:{product_id}:{lang_id}:{permutation_hash} ### Routing - Główny katalog: `admin/` - Template główny: `admin/templates/site/main-layout.php` -- Kontrolery: `autoload/admin/controls/` +- Kontrolery (nowe): `autoload/admin/Controllers/` +- Kontrolery legacy (fallback): `autoload/admin/controls/` ### Przycisk "Wyczyść cache" - **Lokalizacja UI:** `admin/templates/site/main-layout.php:172` @@ -161,7 +162,9 @@ Główna klasa helper z metodami: ## Najważniejsze wzorce ### Namespace'y -- `\admin\controls\` - kontrolery panelu admin +- `\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.) @@ -205,6 +208,11 @@ autoload/ └── front/factory/ # Legacy - stopniowo migrowane ``` +#### Aktualny stan migracji (uzupełnienie) +- Dodane repozytorium: `Domain\Dictionaries\DictionariesRepository` +- Dodane kontrolery DI: `admin\Controllers\DictionariesController`, `admin\Controllers\FilemanagerController` +- `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\` @@ -246,10 +254,34 @@ tests/ │ └── ProductArchiveControllerTest.php # 6 testów └── Integration/ ``` -**Łącznie: 59 tests, 123 assertions** +Aktualnie w suite są też testy modułów `Dictionaries` i `Articles` (repozytoria + kontrolery DI). +**Łącznie: 82 tests, 181 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()` @@ -336,5 +368,5 @@ tests/ - Metoda `clear_product_cache()` w klasie S --- -*Dokument aktualizowany: 2026-02-07* +*Dokument aktualizowany: 2026-02-10* diff --git a/REFACTORING_PLAN.md b/REFACTORING_PLAN.md index 1e46834..003b647 100644 --- a/REFACTORING_PLAN.md +++ b/REFACTORING_PLAN.md @@ -19,7 +19,9 @@ autoload/ │ ├── Banner/ │ │ └── BannerRepository.php # ✅ Zmigrowane (find, delete, save) │ ├── Settings/ -│ │ └── SettingsRepository.php # ✅ Zmigrowane (saveSettings, getSettings) - fasada → factory +│ │ └── SettingsRepository.php # ✅ Zmigrowane (saveSettings, getSettings) - bezposrednio DB +│ ├── Dictionaries/ +│ │ └── DictionariesRepository.php # ✅ Zmigrowane (listForAdmin, find, save, delete, allUnits) │ ├── Cache/ │ │ └── CacheRepository.php # ✅ Zmigrowane (clearCache) │ ├── Order/ @@ -28,7 +30,11 @@ autoload/ │ ├── 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) @@ -134,7 +140,7 @@ grep -r "Product::getQuantity" . - ✅ RedisConnection - singleton - ✅ S::clear_product_cache() - nowa metoda -### 🔄 W trakcie +### 🔄 Status modułów - **Product** - ✅ get_product_quantity() - **ZMIGROWANE** (2025-02-05) 🎉 - Nowa klasa: `Domain\Product\ProductRepository::getQuantity()` @@ -158,10 +164,11 @@ grep -r "Product::getQuantity" . - ✅ 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/`) + - 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`) - - Aktualizacja: ver. 0.241 + - 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) @@ -214,25 +221,41 @@ grep -r "Product::getQuantity" . - 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 - krok pośredni) +- **Settings** (migracja kontrolera) - ✅ SettingsRepository - **ZMIGROWANE** (2026-02-05) 🎉 - Nowa klasa: `Domain\Settings\SettingsRepository` (saveSettings, getSettings) - - Krok pośredni: fasada nad `admin\factory\Settings` (docelowo DI z $db) + - 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 + - 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 + ### 📋 Do zrobienia - Order - Category - ShopAttribute - ShopProduct (factory) +- Pages (`browse_list` i widoki drzewiaste nadal w legacy `admin\controls` / `admin\view`) ## Testowanie @@ -247,17 +270,21 @@ composer require --dev phpunit/phpunit tests/ ├── Unit/ │ ├── Domain/ -│ │ ├── Product/ProductRepositoryTest.php # 15 testów -│ │ ├── Banner/BannerRepositoryTest.php # 4 testy -│ │ ├── Settings/SettingsRepositoryTest.php # 3 testy -│ │ └── Cache/CacheRepositoryTest.php # 4 testy +│ │ ├── Article/ArticleRepositoryTest.php +│ │ ├── Banner/BannerRepositoryTest.php +│ │ ├── Cache/CacheRepositoryTest.php +│ │ ├── Dictionaries/DictionariesRepositoryTest.php +│ │ ├── Product/ProductRepositoryTest.php +│ │ └── Settings/SettingsRepositoryTest.php │ └── admin/ │ └── Controllers/ -│ ├── SettingsControllerTest.php # 7 testów -│ └── ProductArchiveControllerTest.php # 6 testów +│ ├── ArticlesControllerTest.php +│ ├── DictionariesControllerTest.php +│ ├── ProductArchiveControllerTest.php +│ └── SettingsControllerTest.php └── Integration/ ``` -**Łącznie: 48 testów, 91 asercji** +**Łącznie: 82 testów, 181 asercji** ### Przykład testu ```php @@ -337,11 +364,14 @@ vendor/bin/phpstan analyse autoload/Domain - [ ] getFromCache - [ ] getProductImg 3. **Banner** ✅ (pełna migracja kontrolera, ver. 0.239) -4. **Settings** ✅ (migracja kontrolera - krok pośredni, ver. 0.240) -5. **ProductArchive** ✅ (migracja kontrolera + cleanup szablonów, ver. 0.241) -6. **Order** -5. **Category** -6. **ShopAttribute** +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. **Order** +9. **Category** +10. **ShopAttribute** +11. **Pages** (`browse_list` i powiązane widoki nadal legacy) - **Form Edit System** - Nowy uniwersalny system formularzy edycji - ✅ Klasy ViewModel: `FormFieldType`, `FormField`, `FormTab`, `FormAction`, `FormEditViewModel` @@ -352,11 +382,11 @@ vendor/bin/phpstan analyse autoload/Domain - ✅ 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 (Articles, Settings, Product, Category, itd.) + - **Do zrobienia**: Przerobić pozostałe kontrolery/formularze (Product, Category, Pages, itd.) --- *Rozpoczęto: 2025-02-05* -*Ostatnia aktualizacja: 2026-02-08* +*Ostatnia aktualizacja: 2026-02-10* ## Form Edit System - Dokumentacja użycia diff --git a/TESTING.md b/TESTING.md index 324667d..501ceb1 100644 --- a/TESTING.md +++ b/TESTING.md @@ -1,136 +1,140 @@ -# 🧪 Testowanie shopPRO +# Testowanie shopPRO ## Szybki start -### Uruchom wszystkie testy +### Pelny zestaw testow ```bash -./test.bat # Windows CMD (z nazwami testów) -./test-simple.bat # Tylko kropki (szybki) -./test-debug.bat # Pełne szczegóły (debug) -./test.ps1 # PowerShell (autodetekcja PHP) -./test.sh # Git Bash +composer test ``` -### Konkretny plik +Alternatywnie (Windows): ```bash -./test.bat tests/Unit/Domain/Product/ProductRepositoryTest.php +./test.ps1 +./test.bat +./test-simple.bat +./test-debug.bat +``` + +Alternatywnie (Git Bash): +```bash +./test.sh +``` + +### Konkretny plik testowy +```bash +./test.ps1 tests/Unit/Domain/Product/ProductRepositoryTest.php ./test.ps1 tests/Unit/admin/Controllers/ArticlesControllerTest.php ``` -## Tryby wyświetlania +### Konkretny test (`--filter`) +```bash +./test.ps1 --filter testGetQuantityReturnsCorrectValue +``` -### 1. TestDox (domyślny) - Czytelna lista ✅ +## Aktualny stan suite + +Ostatnio zweryfikowano: 2026-02-10 + +```text +OK (82 tests, 181 assertions) +``` + +## Struktura testow + +```text +tests/ +|-- bootstrap.php +|-- Unit/ +| |-- Domain/ +| | |-- Article/ArticleRepositoryTest.php +| | |-- Banner/BannerRepositoryTest.php +| | |-- Cache/CacheRepositoryTest.php +| | |-- Dictionaries/DictionariesRepositoryTest.php +| | |-- Product/ProductRepositoryTest.php +| | `-- Settings/SettingsRepositoryTest.php +| `-- admin/ +| `-- Controllers/ +| |-- ArticlesControllerTest.php +| |-- DictionariesControllerTest.php +| |-- ProductArchiveControllerTest.php +| `-- SettingsControllerTest.php +`-- Integration/ +``` + +## Tryby uruchamiania + +### 1. TestDox (czytelna lista) ```bash ./test.bat ``` -Wynik: -``` -Product Repository - ✔ Get quantity returns correct value [2.78 ms] - ✔ Get quantity returns null when product not found - ✔ Find returns product data +Uruchamia: +```bash +C:\xampp\php\php.exe phpunit.phar --testdox ``` -### 2. Simple - Tylko kropki 📊 +### 2. Standard (kropki) ```bash ./test-simple.bat ``` -Wynik: -``` -..... 5 / 5 (100%) -OK (5 tests, 11 assertions) +Uruchamia: +```bash +C:\xampp\php\php.exe phpunit.phar ``` -### 3. Debug - Wszystkie szczegóły 🔬 +### 3. Debug (pelne logowanie) ```bash ./test-debug.bat ``` -Wynik: -``` -Test 'testGetQuantity' started -Test 'testGetQuantity' ended -... +Uruchamia: +```bash +C:\xampp\php\php.exe phpunit.phar --debug ``` -## Interpretacja wyników - -### ✅ Sukces +### 4. PowerShell (najbardziej niezawodne) +```bash +./test.ps1 ``` -..... 5 / 5 (100%) +- najpierw probuje `php` z PATH +- jesli brak, probuje m.in. `C:\xampp\php\php.exe` +- zawsze dodaje `--do-not-cache-result` -OK (5 tests, 11 assertions) +## Interpretacja wynikow + +```text +. = test przeszedl +E = error (blad wykonania) +F = failure (niezgodna asercja) ``` -- `.` = test przeszedł -- Wszystko działa! -### ❌ Błąd +Przyklad sukcesu: +```text +................................................................. 65 / 82 ( 79%) +................. 82 / 82 (100%) + +OK (82 tests, 181 assertions) ``` -..E.. 5 / 5 (100%) -ERRORS! -Tests: 5, Assertions: 8, Errors: 1. -``` -- `E` = Error - błąd w kodzie -- Sprawdź szczegóły powyżej +## Dodawanie nowych testow -### ❌ Niepowodzenie -``` -..F.. 5 / 5 (100%) +1. Dodaj plik w odpowiednim module, np. `tests/Unit/Domain//Test.php`. +2. Rozszerz `PHPUnit\Framework\TestCase`. +3. Nazwy metod zaczynaj od `test`. +4. Trzymaj sie wzorca AAA: Arrange, Act, Assert. -FAILURES! -Tests: 5, Assertions: 11, Failures: 1. -``` -- `F` = Failure - asercja się nie powiodła -- Oczekiwano innej wartości - -## Przykładowy test +## Mockowanie (przyklad) ```php -public function testGetQuantityReturnsCorrectValue() -{ - // Arrange - Przygotuj - $mockDb = $this->createMock(\medoo::class); - $mockDb->method('get')->willReturn(42); - $repository = new ProductRepository($mockDb); +$mockDb = $this->createMock(\medoo::class); +$mockDb->method('get')->willReturn(42); - // Act - Wykonaj - $quantity = $repository->getQuantity(123); +$repo = new ProductRepository($mockDb); +$value = $repo->getQuantity(123); - // Assert - Sprawdź - $this->assertEquals(42, $quantity); -} +$this->assertEquals(42, $value); ``` -## Dodawanie nowych testów +## Przydatne informacje -1. Utwórz plik w `tests/Unit/Domain/{Module}/{Class}Test.php` -2. Rozszerz `TestCase` -3. Metody testowe zaczynaj od `test` -4. Użyj pattern **AAA** (Arrange, Act, Assert) - -## Asercje - -```php -$this->assertEquals(expected, actual); // Równość -$this->assertIsInt($value); // Typ -$this->assertNull($value); // Null -$this->assertTrue($condition); // Prawda -$this->assertCount(3, $array); // Rozmiar -``` - -## Mockowanie - -```php -// Prosty mock -$mock = $this->createMock(\medoo::class); -$mock->method('get')->willReturn('wartość'); - -// Z weryfikacją -$mock->expects($this->once()) - ->method('get') - ->with($this->equalTo('tabela')) - ->willReturn(42); -``` - ---- -📚 Więcej: [tests/README.md](tests/README.md) +- Konfiguracja PHPUnit: `phpunit.xml` +- Bootstrap testow: `tests/bootstrap.php` +- Dodatkowy opis: `tests/README.md` diff --git a/temp/update_build/ver_0.200_20260211_000158/admin/templates/banners/banners-list-custom-script.php b/temp/update_build/ver_0.200_20260211_000158/admin/templates/banners/banners-list-custom-script.php new file mode 100644 index 0000000..56ab843 --- /dev/null +++ b/temp/update_build/ver_0.200_20260211_000158/admin/templates/banners/banners-list-custom-script.php @@ -0,0 +1,100 @@ + + + diff --git a/temp/update_build/ver_0.200_20260211_000158/admin/templates/banners/banners-list.php b/temp/update_build/ver_0.200_20260211_000158/admin/templates/banners/banners-list.php new file mode 100644 index 0000000..3e70c9a --- /dev/null +++ b/temp/update_build/ver_0.200_20260211_000158/admin/templates/banners/banners-list.php @@ -0,0 +1,5 @@ + $this->viewModel]); ?> + +viewModel->customScriptView)): ?> + viewModel->customScriptView, ['list' => $this->viewModel]); ?> + diff --git a/temp/update_build/ver_0.200_20260211_000158/admin/templates/filemanager/filemanager.php b/temp/update_build/ver_0.200_20260211_000158/admin/templates/filemanager/filemanager.php new file mode 100644 index 0000000..dc2d008 --- /dev/null +++ b/temp/update_build/ver_0.200_20260211_000158/admin/templates/filemanager/filemanager.php @@ -0,0 +1,4 @@ +filemanager_url ?? '/libraries/filemanager-9.14.2/dialog.php')); +?> + diff --git a/temp/update_build/ver_0.200_20260211_000158/admin/templates/product-archive/products-list-custom-script.php b/temp/update_build/ver_0.200_20260211_000158/admin/templates/product-archive/products-list-custom-script.php new file mode 100644 index 0000000..aacad98 --- /dev/null +++ b/temp/update_build/ver_0.200_20260211_000158/admin/templates/product-archive/products-list-custom-script.php @@ -0,0 +1,100 @@ + + + diff --git a/temp/update_build/ver_0.200_20260211_000158/admin/templates/product-archive/products-list.php b/temp/update_build/ver_0.200_20260211_000158/admin/templates/product-archive/products-list.php new file mode 100644 index 0000000..3e70c9a --- /dev/null +++ b/temp/update_build/ver_0.200_20260211_000158/admin/templates/product-archive/products-list.php @@ -0,0 +1,5 @@ + $this->viewModel]); ?> + +viewModel->customScriptView)): ?> + viewModel->customScriptView, ['list' => $this->viewModel]); ?> + diff --git a/temp/update_build/ver_0.200_20260211_000158/autoload/Domain/Product/ProductRepository.php b/temp/update_build/ver_0.200_20260211_000158/autoload/Domain/Product/ProductRepository.php new file mode 100644 index 0000000..e397697 --- /dev/null +++ b/temp/update_build/ver_0.200_20260211_000158/autoload/Domain/Product/ProductRepository.php @@ -0,0 +1,247 @@ +db = $db; + } + + /** + * Pobiera stan magazynowy produktu + * + * @param int $productId ID produktu + * @return int|null Ilość produktu lub null jeśli nie znaleziono + */ + public function getQuantity(int $productId): ?int + { + $quantity = $this->db->get('pp_shop_products', 'quantity', ['id' => $productId]); + + // Medoo zwraca false jeśli nie znaleziono + return $quantity !== false ? (int)$quantity : null; + } + + /** + * Pobiera produkt po ID + * + * @param int $productId ID produktu + * @return array|null Dane produktu lub null + */ + public function find(int $productId): ?array + { + $product = $this->db->get('pp_shop_products', '*', ['id' => $productId]); + return $product ?: null; + } + + /** + * Zwraca liste produktow z archiwum do panelu admin. + * + * @return array{items: array>, total: int} + */ + public function listArchivedForAdmin( + array $filters, + string $sortColumn = 'id', + string $sortDir = 'DESC', + int $page = 1, + int $perPage = 10 + ): array { + $allowedSortColumns = [ + 'id' => 'psp.id', + 'name' => 'name', + 'price_brutto' => 'psp.price_brutto', + 'price_brutto_promo' => 'psp.price_brutto_promo', + 'quantity' => 'psp.quantity', + 'combinations' => 'combinations', + ]; + + $sortSql = $allowedSortColumns[$sortColumn] ?? 'psp.id'; + $sortDir = strtoupper(trim($sortDir)) === 'ASC' ? 'ASC' : 'DESC'; + $page = max(1, $page); + $perPage = min(self::MAX_PER_PAGE, max(1, $perPage)); + $offset = ($page - 1) * $perPage; + + $where = ['psp.archive = 1', 'psp.parent_id IS NULL']; + $params = []; + + $phrase = trim((string)($filters['phrase'] ?? '')); + if (strlen($phrase) > 255) { + $phrase = substr($phrase, 0, 255); + } + + if ($phrase !== '') { + $where[] = '( + psp.ean LIKE :phrase + OR psp.sku LIKE :phrase + OR EXISTS ( + SELECT 1 + FROM pp_shop_products_langs AS pspl2 + WHERE pspl2.product_id = psp.id + AND pspl2.name LIKE :phrase + ) + )'; + $params[':phrase'] = '%' . $phrase . '%'; + } + + $whereSql = implode(' AND ', $where); + + $sqlCount = " + SELECT COUNT(0) + FROM pp_shop_products AS psp + WHERE {$whereSql} + "; + + $stmtCount = $this->db->query($sqlCount, $params); + $countRows = $stmtCount ? $stmtCount->fetchAll() : []; + $total = isset($countRows[0][0]) ? (int)$countRows[0][0] : 0; + + $sql = " + SELECT + psp.id, + psp.price_brutto, + psp.price_brutto_promo, + psp.quantity, + psp.sku, + psp.ean, + ( + SELECT pspl.name + FROM pp_shop_products_langs AS pspl + INNER JOIN pp_langs AS pl ON pl.id = pspl.lang_id + WHERE pspl.product_id = psp.id + AND pspl.name <> '' + ORDER BY pl.o ASC + LIMIT 1 + ) AS name, + ( + SELECT pspi.src + FROM pp_shop_products_images AS pspi + WHERE pspi.product_id = psp.id + ORDER BY pspi.o ASC, pspi.id ASC + LIMIT 1 + ) AS image_src, + ( + SELECT pspi.alt + FROM pp_shop_products_images AS pspi + WHERE pspi.product_id = psp.id + ORDER BY pspi.o ASC, pspi.id ASC + LIMIT 1 + ) AS image_alt, + ( + SELECT COUNT(0) + FROM pp_shop_products AS pspc + WHERE pspc.parent_id = psp.id + ) AS combinations + FROM pp_shop_products AS psp + WHERE {$whereSql} + ORDER BY {$sortSql} {$sortDir}, psp.id {$sortDir} + LIMIT {$perPage} OFFSET {$offset} + "; + + $stmt = $this->db->query($sql, $params); + $items = $stmt ? $stmt->fetchAll() : []; + + return [ + 'items' => is_array($items) ? $items : [], + 'total' => $total, + ]; + } + + /** + * Pobiera cenę produktu (promocyjną jeśli jest niższa, w przeciwnym razie regularną) + * + * @param int $productId ID produktu + * @return float|null Cena brutto lub null jeśli nie znaleziono + */ + public function getPrice(int $productId): ?float + { + $prices = $this->db->get('pp_shop_products', ['price_brutto', 'price_brutto_promo'], ['id' => $productId]); + + if (!$prices) { + return null; + } + + if ($prices['price_brutto_promo'] != '' && $prices['price_brutto_promo'] < $prices['price_brutto']) { + return (float)$prices['price_brutto_promo']; + } + + return (float)$prices['price_brutto']; + } + + /** + * Pobiera nazwę produktu w danym języku + * + * @param int $productId ID produktu + * @param string $langId ID języka + * @return string|null Nazwa produktu lub null jeśli nie znaleziono + */ + public function getName(int $productId, string $langId): ?string + { + $name = $this->db->get('pp_shop_products_langs', 'name', ['AND' => ['product_id' => $productId, 'lang_id' => $langId]]); + + return $name ?: null; + } + + /** + * Aktualizuje ilość produktu + * + * @param int $productId ID produktu + * @param int $quantity Nowa ilość + * @return bool Czy aktualizacja się powiodła + */ + public function updateQuantity(int $productId, int $quantity): bool + { + $result = $this->db->update( + 'pp_shop_products', + ['quantity' => $quantity], + ['id' => $productId] + ); + + return $result !== false; + } + + /** + * Przywraca produkt z archiwum (wraz z kombinacjami) + * + * @param int $productId ID produktu + * @return bool Czy operacja się powiodła + */ + public function unarchive(int $productId): bool + { + $this->db->update( 'pp_shop_products', [ 'status' => 1, 'archive' => 0 ], [ 'id' => $productId ] ); + $this->db->update( 'pp_shop_products', [ 'status' => 1, 'archive' => 0 ], [ 'parent_id' => $productId ] ); + + return true; + } + + /** + * Przenosi produkt do archiwum (wraz z kombinacjami) + * + * @param int $productId ID produktu + * @return bool Czy operacja się powiodła + */ + public function archive(int $productId): bool + { + $this->db->update( 'pp_shop_products', [ 'status' => 0, 'archive' => 1 ], [ 'id' => $productId ] ); + $this->db->update( 'pp_shop_products', [ 'status' => 0, 'archive' => 1 ], [ 'parent_id' => $productId ] ); + + return true; + } +} diff --git a/temp/update_build/ver_0.200_20260211_000158/autoload/admin/Controllers/BannerController.php b/temp/update_build/ver_0.200_20260211_000158/autoload/admin/Controllers/BannerController.php new file mode 100644 index 0000000..666a64c --- /dev/null +++ b/temp/update_build/ver_0.200_20260211_000158/autoload/admin/Controllers/BannerController.php @@ -0,0 +1,337 @@ +repository = $repository; + $this->formHandler = new FormRequestHandler(); + } + + /** + * Lista banerow + */ + public function list(): string + { + $sortableColumns = ['name', 'status', 'home_page', 'date_start', 'date_end']; + + $filterDefinitions = [ + [ + 'key' => 'name', + 'label' => 'Nazwa', + 'type' => 'text', + ], + [ + 'key' => 'status', + 'label' => 'Aktywny', + 'type' => 'select', + 'options' => [ + '' => '- aktywny -', + '1' => 'tak', + '0' => 'nie', + ], + ], + ]; + + $listRequest = \admin\Support\TableListRequestFactory::fromRequest( + $filterDefinitions, + $sortableColumns, + 'name' + ); + + // Historycznie lista banerow domyslnie byla sortowana rosnaco po nazwie. + $sortDir = $listRequest['sortDir']; + if (trim((string)\S::get('sort')) === '') { + $sortDir = 'ASC'; + } + + $result = $this->repository->listForAdmin( + $listRequest['filters'], + $listRequest['sortColumn'], + $sortDir, + $listRequest['page'], + $listRequest['perPage'] + ); + + $rows = []; + $lp = ($listRequest['page'] - 1) * $listRequest['perPage'] + 1; + foreach ($result['items'] as $item) { + $id = (int)$item['id']; + $name = (string)($item['name'] ?? ''); + $homePage = (int)($item['home_page'] ?? 0); + $isActive = (int)($item['status'] ?? 0) === 1; + $thumbnailSrc = trim((string)($item['thumbnail_src'] ?? '')); + if ($thumbnailSrc !== '' && !preg_match('#^(https?:)?//#i', $thumbnailSrc) && strpos($thumbnailSrc, '/') !== 0) { + $thumbnailSrc = '/' . ltrim($thumbnailSrc, '/'); + } + + $thumbnail = '-'; + if ($thumbnailSrc !== '') { + $thumbnail = ''; + } + + $rows[] = [ + 'lp' => $lp++ . '.', + 'thumbnail' => $thumbnail, + 'name' => '' . htmlspecialchars($name, ENT_QUOTES, 'UTF-8') . '', + 'status' => $isActive ? 'tak' : 'nie', + 'home_page' => $homePage === 1 ? 'tak' : 'nie', + 'slider' => $homePage === 1 ? 'nie' : 'tak', + 'date_start' => !empty($item['date_start']) ? date('Y-m-d', strtotime((string)$item['date_start'])) : '-', + 'date_end' => !empty($item['date_end']) ? date('Y-m-d', strtotime((string)$item['date_end'])) : '-', + '_actions' => [ + [ + 'label' => 'Edytuj', + 'url' => '/admin/banners/banner_edit/id=' . $id, + 'class' => 'btn btn-xs btn-primary', + ], + [ + 'label' => 'Usun', + 'url' => '/admin/banners/banner_delete/id=' . $id, + 'class' => 'btn btn-xs btn-danger', + 'confirm' => 'Na pewno chcesz usunac wybrany element?', + ], + ], + ]; + } + + $total = (int)$result['total']; + $totalPages = max(1, (int)ceil($total / $listRequest['perPage'])); + + $viewModel = new \admin\ViewModels\Common\PaginatedTableViewModel( + [ + ['key' => 'lp', 'label' => 'Lp.', 'class' => 'text-center', 'sortable' => false], + ['key' => 'thumbnail', 'label' => 'Miniatura', 'class' => 'text-center', 'sortable' => false, 'raw' => true], + ['key' => 'name', 'sort_key' => 'name', 'label' => 'Nazwa', 'sortable' => true, 'raw' => true], + ['key' => 'status', 'sort_key' => 'status', 'label' => 'Aktywny', 'class' => 'text-center', 'sortable' => true, 'raw' => true], + ['key' => 'home_page', 'sort_key' => 'home_page', 'label' => 'Strona glowna', 'class' => 'text-center', 'sortable' => true, 'raw' => true], + ['key' => 'slider', 'label' => 'Slajder', 'class' => 'text-center', 'sortable' => false, 'raw' => true], + ['key' => 'date_start', 'sort_key' => 'date_start', 'label' => 'Data rozpoczecia', 'class' => 'text-center', 'sortable' => true], + ['key' => 'date_end', 'sort_key' => 'date_end', 'label' => 'Data zakonczenia', 'class' => 'text-center', '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/banners/view_list/', + 'Brak danych w tabeli.', + '/admin/banners/banner_edit/', + 'Dodaj baner', + 'banners/banners-list-custom-script' + ); + + return \Tpl::view('banners/banners-list', [ + 'viewModel' => $viewModel, + ]); + } + + /** + * Edycja banera + */ + public function edit(): string + { + $bannerId = (int)\S::get('id'); + $banner = $this->repository->find($bannerId); + $languages = \admin\factory\Languages::languages_list(); + + // Sprawdź czy są błędy walidacji z poprzedniego requestu + $validationErrors = $_SESSION['form_errors'][$this->getFormId()] ?? null; + if ($validationErrors) { + unset($_SESSION['form_errors'][$this->getFormId()]); + } + + $viewModel = $this->buildFormViewModel($banner, $languages, $validationErrors); + + return \Tpl::view('components/form-edit', ['form' => $viewModel]); + } + + /** + * Zapisanie banera (AJAX) + */ + public function save(): void + { + $response = ['success' => false, 'errors' => []]; + + $bannerId = (int)\S::get('id'); + $banner = $this->repository->find($bannerId); + $languages = \admin\factory\Languages::languages_list(); + + $viewModel = $this->buildFormViewModel($banner, $languages); + + // Przetwórz dane z POST + $result = $this->formHandler->handleSubmit($viewModel, $_POST); + + if (!$result['success']) { + // Zapisz błędy w sesji i zwróć jako JSON + $_SESSION['form_errors'][$this->getFormId()] = $result['errors']; + $response['errors'] = $result['errors']; + echo json_encode($response); + exit; + } + + // Zapisz dane + $data = $result['data']; + $data['id'] = $bannerId ?: null; + + $savedId = $this->repository->save($data); + + if ($savedId) { + \S::delete_dir('../temp/'); + $response = [ + 'success' => true, + 'id' => $savedId, + 'message' => 'Baner został zapisany.' + ]; + } else { + $response['errors'] = ['general' => 'Błąd podczas zapisywania do bazy.']; + } + + echo json_encode($response); + exit; + } + + /** + * Usuniecie banera + */ + public function delete(): void + { + $bannerId = (int)\S::get('id'); + if ($this->repository->delete($bannerId)) { + \S::delete_dir('../temp/'); + \S::alert('Baner zostal usuniety.'); + } + + header('Location: /admin/banners/view_list/'); + exit; + } + + /** + * Buduje model widoku formularza + */ + private function buildFormViewModel(array $banner, array $languages, ?array $errors = null): FormEditViewModel + { + $bannerId = $banner['id'] ?? 0; + $isNew = empty($bannerId); + + // Domyślne wartości dla nowego banera + if ($isNew) { + $banner['status'] = 1; + $banner['home_page'] = 0; + } + + $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', + 'value' => ($banner['status'] ?? 1) == 1, + ]), + FormField::date('date_start', [ + 'label' => 'Data rozpoczęcia', + 'tab' => 'settings', + ]), + FormField::date('date_end', [ + 'label' => 'Data zakończenia', + 'tab' => 'settings', + ]), + FormField::switch('home_page', [ + 'label' => 'Slajder / Strona główna', + 'tab' => 'settings', + 'value' => ($banner['home_page'] ?? 0) == 1, + ]), + + // Sekcja językowa w zakładce Zawartość + FormField::langSection('translations', 'content', [ + FormField::image('src', [ + 'label' => 'Obraz', + 'filemanager' => true, + ]), + FormField::text('url', [ + 'label' => 'Url', + ]), + FormField::textarea('html', [ + 'label' => 'Kod HTML', + 'rows' => 6, + ]), + FormField::editor('text', [ + 'label' => 'Treść', + 'toolbar' => 'MyTool', + 'height' => 300, + ]), + ]), + ]; + + $actions = [ + FormAction::save( + '/admin/banners/banner_save/' . ($isNew ? '' : 'id=' . $bannerId), + '/admin/banners/view_list/' + ), + FormAction::cancel('/admin/banners/view_list/'), + ]; + + return new FormEditViewModel( + $this->getFormId(), + $isNew ? 'Nowy baner' : 'Edycja banera', + $banner, + $fields, + $tabs, + $actions, + 'POST', + '/admin/banners/banner_save/' . ($isNew ? '' : 'id=' . $bannerId), + '/admin/banners/view_list/', + true, + ['id' => $bannerId], + $languages, + $errors + ); + } + + /** + * Zwraca identyfikator formularza + */ + private function getFormId(): string + { + return 'banner-edit'; + } +} diff --git a/temp/update_build/ver_0.200_20260211_000158/autoload/admin/Controllers/FilemanagerController.php b/temp/update_build/ver_0.200_20260211_000158/autoload/admin/Controllers/FilemanagerController.php new file mode 100644 index 0000000..ba2535e --- /dev/null +++ b/temp/update_build/ver_0.200_20260211_000158/autoload/admin/Controllers/FilemanagerController.php @@ -0,0 +1,46 @@ +ensureFilemanagerAccessKey(); + $filemanagerUrl = $this->buildFilemanagerUrl($akey); + + return \Tpl::view('filemanager/filemanager', [ + 'filemanager_url' => $filemanagerUrl, + ]); + } + + private function ensureFilemanagerAccessKey(): string + { + $expiresAt = (int)($_SESSION['rfm_akey_expires'] ?? 0); + $existingKey = trim((string)($_SESSION['rfm_akey'] ?? '')); + + if ($existingKey !== '' && $expiresAt >= time()) { + $_SESSION['rfm_akey_expires'] = time() + self::RFM_KEY_TTL; + return $existingKey; + } + + try { + $newKey = bin2hex(random_bytes(16)); + } catch (\Throwable $e) { + $newKey = sha1(uniqid('rfm', true)); + } + + $_SESSION['rfm_akey'] = $newKey; + $_SESSION['rfm_akey_expires'] = time() + self::RFM_KEY_TTL; + + return $newKey; + } + + private function buildFilemanagerUrl(string $akey): string + { + return self::FILEMANAGER_DIALOG_PATH . '?akey=' . rawurlencode($akey); + } +} + diff --git a/temp/update_build/ver_0.200_20260211_000158/autoload/admin/Controllers/ProductArchiveController.php b/temp/update_build/ver_0.200_20260211_000158/autoload/admin/Controllers/ProductArchiveController.php new file mode 100644 index 0000000..5883b59 --- /dev/null +++ b/temp/update_build/ver_0.200_20260211_000158/autoload/admin/Controllers/ProductArchiveController.php @@ -0,0 +1,165 @@ +productRepository = $productRepository; + } + + public function list(): string + { + $sortableColumns = ['id', 'name', 'price_brutto', 'price_brutto_promo', 'quantity']; + + $filterDefinitions = [ + [ + 'key' => 'phrase', + 'label' => 'Nazwa / EAN / SKU', + 'type' => 'text', + ], + ]; + + $listRequest = \admin\Support\TableListRequestFactory::fromRequest( + $filterDefinitions, + $sortableColumns, + 'id', + [10, 15, 25, 50, 100], + 10 + ); + + $result = $this->productRepository->listArchivedForAdmin( + $listRequest['filters'], + $listRequest['sortColumn'], + $listRequest['sortDir'], + $listRequest['page'], + $listRequest['perPage'] + ); + + $rows = []; + $lp = ($listRequest['page'] - 1) * $listRequest['perPage'] + 1; + foreach ($result['items'] as $item) { + $id = (int)($item['id'] ?? 0); + $name = trim((string)($item['name'] ?? '')); + $sku = trim((string)($item['sku'] ?? '')); + $ean = trim((string)($item['ean'] ?? '')); + $imageSrc = trim((string)($item['image_src'] ?? '')); + $imageAlt = trim((string)($item['image_alt'] ?? '')); + $priceBrutto = (string)($item['price_brutto'] ?? ''); + $priceBruttoPromo = (string)($item['price_brutto_promo'] ?? ''); + $quantity = (int)($item['quantity'] ?? 0); + $combinations = (int)($item['combinations'] ?? 0); + + if ($imageSrc === '') { + $imageSrc = '/admin/layout/images/no-image.png'; + } elseif (!preg_match('#^(https?:)?//#i', $imageSrc) && strpos($imageSrc, '/') !== 0) { + $imageSrc = '/' . ltrim($imageSrc, '/'); + } + + $categories = trim((string)\admin\factory\ShopProduct::product_categories($id)); + $categoriesHtml = ''; + if ($categories !== '') { + $categoriesHtml = '' + . htmlspecialchars($categories, ENT_QUOTES, 'UTF-8') + . ''; + } + + $skuEanParts = []; + if ($sku !== '') { + $skuEanParts[] = 'SKU: ' . htmlspecialchars($sku, ENT_QUOTES, 'UTF-8'); + } + if ($ean !== '') { + $skuEanParts[] = 'EAN: ' . htmlspecialchars($ean, ENT_QUOTES, 'UTF-8'); + } + $skuEanHtml = ''; + if (!empty($skuEanParts)) { + $skuEanHtml = '' . implode(', ', $skuEanParts) . ''; + } + + $productCell = '
' + . '' . htmlspecialchars($imageAlt, ENT_QUOTES, 'UTF-8') . '' + . '
' + . '' + . $categoriesHtml + . $skuEanHtml; + + $rows[] = [ + 'lp' => $lp++ . '.', + 'product' => $productCell, + 'price_brutto' => $priceBrutto !== '' ? $priceBrutto : '-', + 'price_brutto_promo' => $priceBruttoPromo !== '' ? $priceBruttoPromo : '-', + 'quantity' => (string)$quantity, + '_actions' => [ + [ + 'label' => 'Przywroc', + 'url' => '/admin/product_archive/unarchive/product_id=' . $id, + 'class' => 'btn btn-xs btn-success', + 'confirm' => 'Na pewno chcesz przywrocic wybrany produkt z archiwum?', + 'confirm_ok' => 'Przywroc', + 'confirm_cancel' => 'Anuluj', + ], + ], + ]; + } + + $total = (int)$result['total']; + $totalPages = max(1, (int)ceil($total / $listRequest['perPage'])); + + $viewModel = new \admin\ViewModels\Common\PaginatedTableViewModel( + [ + ['key' => 'lp', 'label' => 'Lp.', 'class' => 'text-center', 'sortable' => false], + ['key' => 'product', 'sort_key' => 'name', 'label' => 'Nazwa', 'sortable' => true, 'raw' => true], + ['key' => 'price_brutto', 'sort_key' => 'price_brutto', 'label' => 'Cena', 'class' => 'text-center', 'sortable' => true], + ['key' => 'price_brutto_promo', 'sort_key' => 'price_brutto_promo', 'label' => 'Cena promocyjna', 'class' => 'text-center', 'sortable' => true], + ['key' => 'quantity', 'sort_key' => 'quantity', 'label' => 'Stan MG', 'class' => 'text-center', 'sortable' => true] + ], + $rows, + $listRequest['viewFilters'], + [ + 'column' => $listRequest['sortColumn'], + 'dir' => $listRequest['sortDir'], + ], + [ + 'page' => $listRequest['page'], + 'per_page' => $listRequest['perPage'], + 'total' => $total, + 'total_pages' => $totalPages, + ], + array_merge($listRequest['queryFilters'], [ + 'sort' => $listRequest['sortColumn'], + 'dir' => $listRequest['sortDir'], + 'per_page' => $listRequest['perPage'], + ]), + $listRequest['perPageOptions'], + $sortableColumns, + '/admin/product_archive/products_list/', + 'Brak danych w tabeli.', + null, + null, + 'product-archive/products-list-custom-script' + ); + + return \Tpl::view('product-archive/products-list', [ + 'viewModel' => $viewModel, + ]); + } + + public function unarchive(): void + { + if ( $this->productRepository->unarchive( (int) \S::get( 'product_id' ) ) ) + \S::alert( 'Produkt został przywrócony z archiwum.' ); + else + \S::alert( 'Podczas przywracania produktu z archiwum wystąpił błąd. Proszę spróbować ponownie' ); + + header( 'Location: /admin/product_archive/products_list/' ); + exit; + } +} diff --git a/temp/update_build/ver_0.200_20260211_000158/autoload/admin/class.Site.php b/temp/update_build/ver_0.200_20260211_000158/autoload/admin/class.Site.php new file mode 100644 index 0000000..7d196bd --- /dev/null +++ b/temp/update_build/ver_0.200_20260211_000158/autoload/admin/class.Site.php @@ -0,0 +1,356 @@ + $user['login'], + 'ts' => time() + ]; + + $json = json_encode($payloadArr, JSON_UNESCAPED_SLASHES); + $sig = hash_hmac('sha256', $json, self::APP_SECRET_KEY); + $payload = base64_encode($json . '.' . $sig); + + setcookie( $cookie_name, $payload, [ + 'expires' => time() + (86400 * 14), + 'path' => '/', + 'domain' => $domain, + 'secure' => true, + 'httponly' => true, + 'samesite' => 'Lax', + ]); + } + } + + public static function special_actions() + { + $sa = \S::get('s-action'); + $domain = preg_replace('/^www\./', '', $_SERVER['SERVER_NAME']); + $cookie_name = 'admin_remember_' . str_replace( '.', '-', $domain ); + + switch ($sa) + { + case 'user-logon': + { + $login = \S::get('login'); + $pass = \S::get('password'); + + $result = \admin\factory\Users::logon($login, $pass); + + if ( $result == 1 ) + { + $user = \admin\factory\Users::details($login); + + if ( $user['twofa_enabled'] == 1 ) + { + \S::set_session( 'twofa_pending', [ + 'uid' => (int)$user['id'], + 'login' => $login, + 'remember' => (bool)\S::get('remember'), + 'started' => time(), + ] ); + + if ( !\admin\factory\Users::send_twofa_code( (int)$user['id'] ) ) + { + \S::alert('Nie udało się wysłać kodu 2FA. Spróbuj ponownie.'); + \S::delete_session('twofa_pending'); + header('Location: /admin/'); + exit; + } + + header('Location: /admin/user/twofa/'); + exit; + } + else + { + $user = \admin\factory\Users::details($login); + + self::finalize_admin_login( + $user, + $domain, + $cookie_name, + (bool)\S::get('remember') + ); + + header('Location: /admin/articles/view_list/'); + exit; + } + } + else + { + if ($result == -1) + { + \S::alert('Z powodu 5 nieudanych prób Twoje konto zostało zablokowane.'); + } + else + { + \S::alert('Podane hasło jest nieprawidłowe lub użytkownik nie istnieje.'); + } + header('Location: /admin/'); + exit; + } + } + break; + + case 'user-2fa-verify': + { + $pending = \S::get_session('twofa_pending'); + if ( !$pending || empty( $pending['uid'] ) ) { + \S::alert('Sesja 2FA wygasła. Zaloguj się ponownie.'); + header('Location: /admin/'); + exit; + } + + $code = trim((string)\S::get('twofa')); + if (!preg_match('/^\d{6}$/', $code)) + { + \S::alert('Nieprawidłowy format kodu.'); + header('Location: /admin/user/twofa/'); + exit; + } + + $ok = \admin\factory\Users::verify_twofa_code((int)$pending['uid'], $code); + if (!$ok) + { + \S::alert('Błędny lub wygasły kod.'); + header('Location: /admin/user/twofa/'); + exit; + } + + // 2FA OK — finalna sesja + $user = \admin\factory\Users::details($pending['login']); + + self::finalize_admin_login( + $user, + $domain, + $cookie_name, + $pending['remember'] ? true : false + ); + + header('Location: /admin/articles/view_list/'); + exit; + } + break; + + case 'user-2fa-resend': + { + $pending = \S::get_session('twofa_pending'); + if (!$pending || empty($pending['uid'])) + { + \S::alert('Sesja 2FA wygasła. Zaloguj się ponownie.'); + header('Location: /admin/'); + exit; + } + + if (!\admin\factory\Users::send_twofa_code((int)$pending['uid'], true)) + { + \S::alert('Kod można wysłać ponownie po krótkiej przerwie.'); + } + else + { + \S::alert('Nowy kod został wysłany.'); + } + header('Location: /admin/user/twofa/'); + exit; + } + break; + + case 'user-logout': + { + setcookie($cookie_name, "", [ + 'expires' => time() - 86400, + 'path' => '/', + 'domain' => $domain, + 'secure' => true, + 'httponly' => true, + 'samesite' => 'Lax', + ]); + \S::delete_session('twofa_pending'); + session_destroy(); + header('Location: /admin/'); + exit; + } + break; + } + } + + /** + * Mapa nowych kontrolerów: module => fabryka kontrolera (DI) + * Przy migracji kolejnego kontrolera - dodaj wpis tutaj + */ + private static $newControllers = []; + + /** + * Zwraca mapę fabryk kontrolerów (inicjalizacja runtime) + */ + private static function getControllerFactories(): array + { + if ( !empty( self::$newControllers ) ) + return self::$newControllers; + + self::$newControllers = [ + 'Articles' => function() { + global $mdb; + + return new \admin\Controllers\ArticlesController( + new \Domain\Article\ArticleRepository( $mdb ) + ); + }, + 'Banners' => function() { + global $mdb; + + return new \admin\Controllers\BannerController( + new \Domain\Banner\BannerRepository( $mdb ) + ); + }, + 'Settings' => function() { + global $mdb; + + return new \admin\Controllers\SettingsController( + new \Domain\Settings\SettingsRepository( $mdb ) + ); + }, + 'ProductArchive' => function() { + global $mdb; + + return new \admin\Controllers\ProductArchiveController( + new \Domain\Product\ProductRepository( $mdb ) + ); + }, + // Alias dla starego modułu /admin/archive/products_list/ + 'Archive' => function() { + global $mdb; + + return new \admin\Controllers\ProductArchiveController( + new \Domain\Product\ProductRepository( $mdb ) + ); + }, + 'Dictionaries' => function() { + global $mdb; + + return new \admin\Controllers\DictionariesController( + new \Domain\Dictionaries\DictionariesRepository( $mdb ) + ); + }, + 'Filemanager' => function() { + return new \admin\Controllers\FilemanagerController(); + }, + ]; + + return self::$newControllers; + } + + /** + * Tworzy instancję nowego kontrolera z Dependency Injection + */ + private static function createController( string $moduleName ) + { + global $mdb; + + $factories = self::getControllerFactories(); + if ( !isset( $factories[$moduleName] ) ) + return null; + + $factory = $factories[$moduleName]; + if ( !is_callable( $factory ) ) + return null; + + return $factory(); + } + + /** + * Mapowanie nazw akcji: stara_nazwa => nowa_nazwa + * Potrzebne gdy stary routing używa innej konwencji nazw + */ + private static $actionMap = [ + 'gallery_order_save' => 'galleryOrderSave', + 'view_list' => 'list', + 'article_edit' => 'edit', + 'article_save' => 'save', + 'article_delete' => 'delete', + 'banner_edit' => 'edit', + 'banner_save' => 'save', + 'banner_delete' => 'delete', + 'clear_cache' => 'clearCache', + 'clear_cache_ajax' => 'clearCacheAjax', + 'settings_save' => 'save', + 'products_list' => 'list', + 'unit_edit' => 'edit', + 'unit_save' => 'save', + 'unit_delete' => 'delete', + ]; + + public static function route() + { + $_SESSION['admin'] = true; + + if ( \S::get( 'p' ) ) + \S::set_session( 'p' , \S::get( 'p' ) ); + + $page = \S::get_session( 'p' ); + + // Budowanie nazwy modułu + $moduleName = ''; + $results = explode( '_', \S::get( 'module' ) ); + if ( is_array( $results ) ) foreach ( $results as $row ) + $moduleName .= ucfirst( $row ); + + $action = \S::get( 'action' ); + + // 1. Sprawdź czy istnieje nowy kontroler + $factories = self::getControllerFactories(); + if ( isset( $factories[$moduleName] ) ) + { + $controller = self::createController( $moduleName ); + if ( $controller ) + { + // Mapuj nazwę akcji (stara → nowa) lub użyj oryginalnej + $newAction = self::$actionMap[$action] ?? $action; + + if ( method_exists( $controller, $newAction ) ) + { + return $controller->$newAction(); + } + } + + } + + // 2. Fallback na stary kontroler + $class = '\admin\controls\\' . $moduleName; + + if ( class_exists( $class ) and method_exists( new $class, $action ) ) + return call_user_func_array( array( $class, $action ), array() ); + else + { + \S::alert( 'Nieprawidłowy adres url.' ); + return false; + } + } + + static public function update() + { + global $mdb; + + if ( $results = $mdb -> select( 'pp_updates', [ 'name' ], [ 'done' => 0 ] ) ) + { + foreach ( $results as $row ) + { + $class = '\admin\factory\Update'; + $method = $row['name']; + + if ( class_exists( $class ) and method_exists( new $class, $method ) ) + call_user_func_array( array( $class, $method ), array() ); + } + } + } +} diff --git a/temp/update_build/ver_0.200_20260211_000158/autoload/admin/controls/class.ShopProduct.php b/temp/update_build/ver_0.200_20260211_000158/autoload/admin/controls/class.ShopProduct.php new file mode 100644 index 0000000..e076736 --- /dev/null +++ b/temp/update_build/ver_0.200_20260211_000158/autoload/admin/controls/class.ShopProduct.php @@ -0,0 +1,414 @@ + update( 'pp_shop_products', [ 'price_brutto_promo' => $price_brutto_promo, 'price_netto_promo' => $price_netto_promo ], [ 'id' => \S::get( 'products' )[0] ] ); + + \admin\factory\ShopProduct::update_product_combinations_prices( \S::get( 'products' )[0], $price_netto, $vat, $price_netto_promo ); + + echo json_encode( [ 'status' => 'ok', 'price_brutto_promo' => $price_brutto_promo, 'price_brutto' => $price_brutto ] ); + exit; + } + echo json_encode( [ 'status' => 'error' ] ); + exit; + } + + // get_products_by_category + static public function get_products_by_category() { + global $mdb; + + $products = $mdb -> select( 'pp_shop_products_categories', 'product_id', [ 'category_id' => \S::get( 'category_id' ) ] ); + + echo json_encode( [ 'status' => 'ok', 'products' => $products ] ); + exit; + } + + static public function mass_edit() + { + return \Tpl::view( 'shop-product/mass-edit', [ + 'products' => \admin\factory\ShopProduct::products_list(), + 'categories' => \admin\factory\ShopCategory::subcategories( null ), + 'dlang' => \front\factory\Languages::default_language() + ] ); + } + + static public function generate_combination() + { + foreach ( $_POST as $key => $val ) + { + if ( strpos( $key, 'attribute_' ) !== false ) + { + $attribute = explode( 'attribute_', $key ); + $attributes[ $attribute[1] ] = $val; + } + } + + if ( \admin\factory\ShopProduct::generate_permutation( (int) \S::get( 'product_id' ), $attributes ) ) + \S::alert( 'Kombinacje produktu zostały wygenerowane.' ); + + header( 'Location: /admin/shop_product/product_combination/product_id=' . (int) \S::get( 'product_id' ) ); + exit; + } + + //usunięcie kombinacji produktu + static public function delete_combination() + { + if ( \admin\factory\ShopProduct::delete_combination( (int)\S::get( 'combination_id' ) ) ) + \S::alert( 'Kombinacja produktu została usunięta' ); + else + \S::alert( 'Podczas usuwania kombinacji produktu wystąpił błąd. Proszę spróbować ponownie' ); + + header( 'Location: /admin/shop_product/product_combination/product_id=' . \S::get( 'product_id' ) ); + exit; + } + + static public function duplicate_product() + { + if ( \admin\factory\ShopProduct::duplicate_product( (int)\S::get( 'product-id' ), (int)\S::get( 'combination' ) ) ) + \S::set_message( 'Produkt został zduplikowany.' ); + else + \S::alert( 'Podczas duplikowania produktu wystąpił błąd. Proszę spróbować ponownie' ); + + header( 'Location: /admin/shop_product/view_list/' ); + exit; + } + + public static function image_delete() + { + $response = [ 'status' => 'error', 'msg' => 'Podczas usuwania zdjecia wystąpił błąd. Proszę spróbować ponownie.' ]; + + if ( \admin\factory\ShopProduct::delete_img( \S::get( 'image_id' ) ) ) + $response = [ 'status' => 'ok' ]; + + echo json_encode( $response ); + exit; + } + + public static function images_order_save() + { + if ( \admin\factory\ShopProduct::images_order_save( \S::get( 'product_id' ), \S::get( 'order' ) ) ) + echo json_encode( [ 'status' => 'ok', 'msg' => 'Produkt został zapisany.' ] ); + + exit; + } + + public static function image_alt_change() + { + $response = [ 'status' => 'error', 'msg' => 'Podczas zmiany atrybutu alt zdjęcia wystąpił błąd. Proszę spróbować ponownie.' ]; + + if ( \admin\factory\ShopProduct::image_alt_change( \S::get( 'image_id' ), \S::get( 'image_alt' ) ) ) + $response = [ 'status' => 'ok' ]; + + echo json_encode( $response ); + exit; + } + + // szybka zmiana statusu produktu + static public function change_product_status() { + + if ( \admin\factory\ShopProduct::change_product_status( (int)\S::get( 'product-id' ) ) ) + \S::set_message( 'Status produktu został zmieniony' ); + + header( 'Location: ' . $_SERVER['HTTP_REFERER'] ); + exit; + } + + // szybka zmiana google xml label + static public function product_change_custom_label() + { + $response = [ 'status' => 'error', 'msg' => 'Podczas zmiany google xml label wystąpił błąd. Proszę spróbować ponownie.' ]; + + if ( \admin\factory\ShopProduct::product_change_custom_label( (int) \S::get( 'product_id' ), \S::get( 'custom_label' ), \S::get( 'value' ) ) ) + $response = [ 'status' => 'ok' ]; + + echo json_encode( $response ); + exit; + } + + // szybka zmiana ceny promocyjnej + static public function product_change_price_brutto_promo() + { + $response = [ 'status' => 'error', 'msg' => 'Podczas zmiany ceny wystąpił błąd. Proszę spróbować ponownie.' ]; + + if ( \admin\factory\ShopProduct::product_change_price_brutto_promo( (int) \S::get( 'product_id' ), \S::get( 'price' ) ) ) + $response = [ 'status' => 'ok' ]; + + echo json_encode( $response ); + exit; + } + + // szybka zmiana ceny + static public function product_change_price_brutto() + { + $response = [ 'status' => 'error', 'msg' => 'Podczas zmiany ceny wystąpił błąd. Proszę spróbować ponownie.' ]; + + if ( \admin\factory\ShopProduct::product_change_price_brutto( (int) \S::get( 'product_id' ), \S::get( 'price' ) ) ) + $response = [ 'status' => 'ok' ]; + + echo json_encode( $response ); + exit; + } + + // pobierz bezpośredni url produktu + static public function ajax_product_url() + { + echo json_encode( [ 'url' => \shop\Product::getProductUrl( \S::get( 'product_id' ) ) ] ); + exit; + } + + // zapisanie produktu + public static function save() + { + $response = [ 'status' => 'error', 'msg' => 'Podczas zapisywania produktu wystąpił błąd. Proszę spróbować ponownie.' ]; + $values = json_decode( \S::get( 'values' ), true ); + + if ( $id = \admin\factory\ShopProduct::save( + $values['id'], $values['name'], $values['short_description'], $values['description'], $values['status'], $values['meta_description'], $values['meta_keywords'], $values['seo_link'], + $values['copy_from'], $values['categories'], $values['price_netto'], $values['price_brutto'], $values['vat'], $values['promoted'], $values['warehouse_message_zero'], $values['warehouse_message_nonzero'], $values['tab_name_1'], + $values['tab_description_1'], $values['tab_name_2'], $values['tab_description_2'], $values['layout_id'], $values['products_related'], (int) $values['set'], $values['price_netto_promo'], $values['price_brutto_promo'], + $values['new_to_date'], $values['stock_0_buy'], $values['wp'], $values['custom_label_0'], $values['custom_label_1'], $values['custom_label_2'], $values['custom_label_3'], $values['custom_label_4'], $values['additional_message'], (int)$values['quantity'], $values['additional_message_text'], $values['additional_message_required'] == 'on' ? 1 : 0, $values['canonical'], $values['meta_title'], $values['producer_id'], $values['sku'], $values['ean'], $values['product_unit'], $values['weight'], $values['xml_name'], $values['custom_field_name'], $values['custom_field_required'], $values['security_information'], $values['custom_field_type'] + ) ) { + $response = [ 'status' => 'ok', 'msg' => 'Produkt został zapisany.', 'id' => $id ]; + } + + echo json_encode( $response ); + exit; + } + + // product_unarchive + static public function product_unarchive() + { + if ( \admin\factory\ShopProduct::product_unarchive( (int) \S::get( 'product_id' ) ) ) + \S::alert( 'Produkt został przywrócony z archiwum.' ); + else + \S::alert( 'Podczas przywracania produktu z archiwum wystąpił błąd. Proszę spróbować ponownie' ); + + header( 'Location: /admin/product_archive/products_list/' ); + exit; + } + + static public function product_archive() + { + if ( \admin\factory\ShopProduct::product_archive( (int) \S::get( 'product_id' ) ) ) + \S::alert( 'Produkt został przeniesiony do archiwum.' ); + else + \S::alert( 'Podczas przenoszenia produktu do archiwum wystąpił błąd. Proszę spróbować ponownie' ); + + header( 'Location: /admin/shop_product/view_list/' ); + exit; + } + + public static function product_delete() + { + if ( \admin\factory\ShopProduct::product_delete( (int) \S::get( 'id' ) ) ) + \S::set_message( 'Produkt został usunięty.' ); + else + \S::alert( 'Podczas usuwania produktu wystąpił błąd. Proszę spróbować ponownie' ); + header( 'Location: /admin/shop_product/view_list/' ); + exit; + } + + // edycja produktu + public static function product_edit() { + global $user, $mdb; + + if ( !$user ) { + header( 'Location: /admin/' ); + exit; + } + + \admin\factory\ShopProduct::delete_nonassigned_images(); + \admin\factory\ShopProduct::delete_nonassigned_files(); + + return \Tpl::view( 'shop-product/product-edit', [ + 'product' => \admin\factory\ShopProduct::product_details( (int) \S::get( 'id' ) ), + 'languages' => \admin\factory\Languages::languages_list(), + 'categories' => \admin\factory\ShopCategory::subcategories( null ), + 'layouts' => \admin\factory\Layouts::layouts_list(), + 'products' => \admin\factory\ShopProduct::products_list(), + 'dlang' => \front\factory\Languages::default_language(), + 'sets' => \shop\ProductSet::sets_list(), + 'producers' => \admin\factory\ShopProducer::all(), + 'units' => ( new \Domain\Dictionaries\DictionariesRepository( $mdb ) ) -> allUnits(), + 'user' => $user + ] ); + } + + // ajax_load_products ARCHIVE + static public function ajax_load_products_archive() + { + echo json_encode( [ + 'status' => 'deprecated', + 'msg' => 'Endpoint nie jest juz wspierany. Uzyj /admin/product_archive/products_list/.', + 'redirect_url' => '/admin/product_archive/products_list/' + ] ); + exit; + } + + // ajax_load_products + static public function ajax_load_products() { + + $response = [ 'status' => 'error', 'msg' => 'Podczas ładowania produktów wystąpił błąd. Proszę spróbować ponownie.' ]; + + \S::set_session( 'products_list_current_page', \S::get( 'current_page' ) ); + \S::set_session( 'products_list_query', \S::get( 'query' ) ); + + if ( $products = \admin\factory\ShopProduct::ajax_products_list( \S::get_session( 'products_list_current_page' ), \S::get_session( 'products_list_query' ) ) ) { + $response = [ + 'status' => 'ok', + 'pagination_max' => ceil( $products['products_count'] / 10 ), + 'html' => \Tpl::view( 'shop-product/products-list-table', [ + 'products' => $products['products'], + 'current_page' => \S::get( 'current_page' ), + 'baselinker_enabled' => \admin\factory\Integrations::baselinker_settings( 'enabled' ), + 'apilo_enabled' => \admin\factory\Integrations::apilo_settings( 'enabled' ), + 'sellasist_enabled' => \admin\factory\Integrations::sellasist_settings( 'enabled' ), + 'show_xml_data' => \S::get_session( 'show_xml_data' ) + ] ) + ]; + } + + echo json_encode( $response ); + exit; + } + + static public function view_list() + { + $current_page = \S::get_session( 'products_list_current_page' ); + + if ( !$current_page ) { + $current_page = 1; + \S::set_session( 'products_list_current_page', $current_page ); + } + + $query = \S::get_session( 'products_list_query' ); + if ( $query ) { + $query_array = []; + parse_str( $query, $query_array ); + } + + if ( \S::get( 'show_xml_data' ) === 'true' ) { + \S::set_session( 'show_xml_data', true ); + } else if ( \S::get( 'show_xml_data' ) === 'false' ) { + \S::set_session( 'show_xml_data', false ); + } + + return \Tpl::view( 'shop-product/products-list', [ + 'current_page' => $current_page, + 'query_array' => $query_array, + 'pagination_max' => ceil( \admin\factory\ShopProduct::count_product() / 10 ), + 'baselinker_enabled' => \admin\factory\Integrations::baselinker_settings( 'enabled' ), + 'apilo_enabled' => \admin\factory\Integrations::apilo_settings( 'enabled' ), + 'sellasist_enabled' => \admin\factory\Integrations::sellasist_settings( 'enabled' ), + 'show_xml_data' => \S::get_session( 'show_xml_data' ), + 'shoppro_enabled' => \admin\factory\Integrations::shoppro_settings( 'enabled' ) + ] ); + } + + // + // KOMBINACJE PRODUKTU + // + + // zapisanie możliwości zakupu przy stanie 0 w kombinacji produktu + static public function product_combination_stock_0_buy_save() + { + \admin\factory\ShopProduct::product_combination_stock_0_buy_save( (int)\S::get( 'product_id' ), \S::get( 'stock_0_buy' ) ); + exit; + } + + // zapisanie sku w kombinacji produktu + static public function product_combination_sku_save() + { + \admin\factory\ShopProduct::product_combination_sku_save( (int)\S::get( 'product_id' ), \S::get( 'sku' ) ); + exit; + } + + // zapisanie ilości w kombinacji produktu + static public function product_combination_quantity_save() + { + \admin\factory\ShopProduct::product_combination_quantity_save( (int)\S::get( 'product_id' ), \S::get( 'quantity' ) ); + exit; + } + + // zapisanie ceny w kombinacji produktu + static public function product_combination_price_save() + { + \admin\factory\ShopProduct::product_combination_price_save( (int)\S::get( 'product_id' ), \S::get( 'price' ) ); + exit; + } + + //wyświetlenie kombinacji produktu + static public function product_combination() + { + return \Tpl::view( 'shop-product/product-combination', [ + 'product' => \admin\factory\ShopProduct::product_details( (int) \S::get( 'product_id' ) ), + 'attributes' => \admin\factory\ShopAttribute::get_attributes_list(), + 'default_language' => \front\factory\Languages::default_language(), + 'product_permutations' => \admin\factory\ShopProduct::get_product_permutations( (int) \S::get( 'product_id' ) ) + ] ); + } + + // generate_sku_code + static public function generate_sku_code() { + $response = [ 'status' => 'error', 'msg' => 'Podczas generowania kodu sku wystąpił błąd. Proszę spróbować ponownie.' ]; + + if ( $sku = \shop\Product::generate_sku_code( \S::get( 'product_id' ) ) ) + $response = [ 'status' => 'ok', 'sku' => $sku ]; + + echo json_encode( $response ); + exit; + } + + // product_xml_name_save + static public function product_xml_name_save() { + $response = [ 'status' => 'error', 'msg' => 'Podczas zapisywania nazwy produktu wystąpił błąd. Proszę spróbować ponownie.' ]; + + if ( \shop\Product::product_xml_name_save( \S::get( 'product_id' ), \S::get( 'product_xml_name' ), \S::get( 'lang_id' ) ) ) + $response = [ 'status' => 'ok' ]; + + echo json_encode( $response ); + exit; + } + + // product_custom_label_suggestions + static public function product_custom_label_suggestions() { + $response = [ 'status' => 'error', 'msg' => 'Podczas pobierania sugestii dla custom label wystąpił błąd. Proszę spróbować ponownie.' ]; + + if ( $suggestions = \shop\Product::product_custom_label_suggestions( \S::get( 'custom_label' ), \S::get( 'label_type' ) ) ) + $response = [ 'status' => 'ok', 'suggestions' => $suggestions ]; + + echo json_encode( $response ); + exit; + } + + // product_custom_label_save + static public function product_custom_label_save() { + $response = [ 'status' => 'error', 'msg' => 'Podczas zapisywania custom label wystąpił błąd. Proszę spróbować ponownie.' ]; + + if ( \shop\Product::product_custom_label_save( \S::get( 'product_id' ), \S::get( 'custom_label' ), \S::get( 'label_type' ) ) ) + $response = [ 'status' => 'ok' ]; + + echo json_encode( $response ); + exit; + } +}