Add new controllers for file management and product archiving
- Introduced FilemanagerController to handle file manager access and URL generation. - Added ProductArchiveController for managing archived products, including listing and unarchiving functionality. - Implemented Site class with methods for finalizing admin login and handling special actions. - Created ShopProduct control class for managing product operations, including mass editing, product duplication, and image handling. - Added necessary methods for product management, including saving, deleting, and changing product statuses.
This commit is contained in:
22
AGENTS.md
22
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.
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -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*
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
190
TESTING.md
190
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/<Module>/<Class>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);
|
||||
|
||||
// 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`
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
<style type="text/css">
|
||||
.banner-thumb-wrap {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.banner-thumb-image {
|
||||
width: 72px;
|
||||
height: 42px;
|
||||
object-fit: cover;
|
||||
border-radius: 4px;
|
||||
cursor: zoom-in;
|
||||
}
|
||||
|
||||
.banner-thumb-popup {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: min(70vw, 760px);
|
||||
max-height: 80vh;
|
||||
padding: 6px;
|
||||
border-radius: 6px;
|
||||
background: #fff;
|
||||
box-shadow: 0 14px 30px rgba(0, 0, 0, .35);
|
||||
z-index: 3000;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
pointer-events: none;
|
||||
transition: opacity .1s ease;
|
||||
}
|
||||
|
||||
.banner-thumb-popup.is-visible {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.banner-thumb-popup img {
|
||||
display: block;
|
||||
width: 100%;
|
||||
max-height: calc(80vh - 12px);
|
||||
object-fit: contain;
|
||||
border-radius: 4px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script type="text/javascript">
|
||||
(function($) {
|
||||
if (!$) {
|
||||
return;
|
||||
}
|
||||
|
||||
$('.banner-thumb-popup').remove();
|
||||
var $popup = $('<div class="banner-thumb-popup" aria-hidden="true"><img src="" alt=""></div>');
|
||||
var $popupImage = $popup.find('img');
|
||||
$('body').append($popup);
|
||||
|
||||
function positionPopup(event) {
|
||||
var offset = 18;
|
||||
var viewportWidth = $(window).width();
|
||||
var viewportHeight = $(window).height();
|
||||
var popupWidth = $popup.outerWidth();
|
||||
var popupHeight = $popup.outerHeight();
|
||||
var left = (event.clientX || 0) + offset;
|
||||
var top = (event.clientY || 0) + offset;
|
||||
|
||||
if (left + popupWidth + 12 > viewportWidth) {
|
||||
left = Math.max(12, (event.clientX || 0) - popupWidth - offset);
|
||||
}
|
||||
|
||||
if (top + popupHeight + 12 > viewportHeight) {
|
||||
top = Math.max(12, viewportHeight - popupHeight - 12);
|
||||
}
|
||||
|
||||
$popup.css({ left: left + 'px', top: top + 'px' });
|
||||
}
|
||||
|
||||
$(document).off('.bannerThumbPopup');
|
||||
|
||||
$(document).on('mouseenter.bannerThumbPopup', '.js-banner-thumb-preview', function(event) {
|
||||
var src = $(this).data('previewSrc');
|
||||
if (!src) {
|
||||
return;
|
||||
}
|
||||
|
||||
$popupImage.attr('src', String(src));
|
||||
$popup.addClass('is-visible');
|
||||
positionPopup(event);
|
||||
});
|
||||
|
||||
$(document).on('mousemove.bannerThumbPopup', '.js-banner-thumb-preview', function(event) {
|
||||
if ($popup.hasClass('is-visible')) {
|
||||
positionPopup(event);
|
||||
}
|
||||
});
|
||||
|
||||
$(document).on('mouseleave.bannerThumbPopup', '.js-banner-thumb-preview', function() {
|
||||
$popup.removeClass('is-visible');
|
||||
$popupImage.attr('src', '');
|
||||
});
|
||||
})(window.jQuery);
|
||||
</script>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?= \Tpl::view('components/table-list', ['list' => $this->viewModel]); ?>
|
||||
|
||||
<?php if (!empty($this->viewModel->customScriptView)): ?>
|
||||
<?= \Tpl::view($this->viewModel->customScriptView, ['list' => $this->viewModel]); ?>
|
||||
<?php endif; ?>
|
||||
@@ -0,0 +1,4 @@
|
||||
<?php
|
||||
$filemanagerUrl = trim((string)($this->filemanager_url ?? '/libraries/filemanager-9.14.2/dialog.php'));
|
||||
?>
|
||||
<iframe src="<?= htmlspecialchars($filemanagerUrl, ENT_QUOTES, 'UTF-8'); ?>" style="border: 0px; width: 100%; height: 800px; background: #FFF; padding: 5px;"></iframe>
|
||||
@@ -0,0 +1,100 @@
|
||||
<style type="text/css">
|
||||
.product-archive-thumb-wrap {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.product-archive-thumb-image {
|
||||
width: 72px;
|
||||
height: 42px;
|
||||
object-fit: cover;
|
||||
border-radius: 4px;
|
||||
cursor: zoom-in;
|
||||
}
|
||||
|
||||
.product-archive-thumb-popup {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: min(70vw, 760px);
|
||||
max-height: 80vh;
|
||||
padding: 6px;
|
||||
border-radius: 6px;
|
||||
background: #fff;
|
||||
box-shadow: 0 14px 30px rgba(0, 0, 0, .35);
|
||||
z-index: 3000;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
pointer-events: none;
|
||||
transition: opacity .1s ease;
|
||||
}
|
||||
|
||||
.product-archive-thumb-popup.is-visible {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.product-archive-thumb-popup img {
|
||||
display: block;
|
||||
width: 100%;
|
||||
max-height: calc(80vh - 12px);
|
||||
object-fit: contain;
|
||||
border-radius: 4px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script type="text/javascript">
|
||||
(function($) {
|
||||
if (!$) {
|
||||
return;
|
||||
}
|
||||
|
||||
$('.product-archive-thumb-popup').remove();
|
||||
var $popup = $('<div class="product-archive-thumb-popup" aria-hidden="true"><img src="" alt=""></div>');
|
||||
var $popupImage = $popup.find('img');
|
||||
$('body').append($popup);
|
||||
|
||||
function positionPopup(event) {
|
||||
var offset = 18;
|
||||
var viewportWidth = $(window).width();
|
||||
var viewportHeight = $(window).height();
|
||||
var popupWidth = $popup.outerWidth();
|
||||
var popupHeight = $popup.outerHeight();
|
||||
var left = (event.clientX || 0) + offset;
|
||||
var top = (event.clientY || 0) + offset;
|
||||
|
||||
if (left + popupWidth + 12 > viewportWidth) {
|
||||
left = Math.max(12, (event.clientX || 0) - popupWidth - offset);
|
||||
}
|
||||
|
||||
if (top + popupHeight + 12 > viewportHeight) {
|
||||
top = Math.max(12, viewportHeight - popupHeight - 12);
|
||||
}
|
||||
|
||||
$popup.css({ left: left + 'px', top: top + 'px' });
|
||||
}
|
||||
|
||||
$(document).off('.productArchiveThumbPopup');
|
||||
|
||||
$(document).on('mouseenter.productArchiveThumbPopup', '.js-product-archive-thumb-preview', function(event) {
|
||||
var src = $(this).data('previewSrc');
|
||||
if (!src) {
|
||||
return;
|
||||
}
|
||||
|
||||
$popupImage.attr('src', String(src));
|
||||
$popup.addClass('is-visible');
|
||||
positionPopup(event);
|
||||
});
|
||||
|
||||
$(document).on('mousemove.productArchiveThumbPopup', '.js-product-archive-thumb-preview', function(event) {
|
||||
if ($popup.hasClass('is-visible')) {
|
||||
positionPopup(event);
|
||||
}
|
||||
});
|
||||
|
||||
$(document).on('mouseleave.productArchiveThumbPopup', '.js-product-archive-thumb-preview', function() {
|
||||
$popup.removeClass('is-visible');
|
||||
$popupImage.attr('src', '');
|
||||
});
|
||||
})(window.jQuery);
|
||||
</script>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?= \Tpl::view('components/table-list', ['list' => $this->viewModel]); ?>
|
||||
|
||||
<?php if (!empty($this->viewModel->customScriptView)): ?>
|
||||
<?= \Tpl::view($this->viewModel->customScriptView, ['list' => $this->viewModel]); ?>
|
||||
<?php endif; ?>
|
||||
@@ -0,0 +1,247 @@
|
||||
<?php
|
||||
namespace Domain\Product;
|
||||
|
||||
/**
|
||||
* Repository odpowiedzialny za dostęp do danych produktów
|
||||
*
|
||||
* Zgodnie z wzorcem Repository Pattern, ta klasa enkapsuluje
|
||||
* logikę dostępu do bazy danych dla produktów.
|
||||
*/
|
||||
class ProductRepository
|
||||
{
|
||||
private const MAX_PER_PAGE = 100;
|
||||
|
||||
/**
|
||||
* @var \medoo Instancja Medoo ORM
|
||||
*/
|
||||
private $db;
|
||||
|
||||
/**
|
||||
* Konstruktor - przyjmuje instancję bazy danych
|
||||
*
|
||||
* @param \medoo $db Instancja Medoo ORM
|
||||
*/
|
||||
public function __construct($db)
|
||||
{
|
||||
$this->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<int, array<string, mixed>>, 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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,337 @@
|
||||
<?php
|
||||
namespace admin\Controllers;
|
||||
|
||||
use Domain\Banner\BannerRepository;
|
||||
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
|
||||
{
|
||||
private BannerRepository $repository;
|
||||
private FormRequestHandler $formHandler;
|
||||
|
||||
public function __construct(BannerRepository $repository)
|
||||
{
|
||||
$this->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 = '<span class="text-muted">-</span>';
|
||||
if ($thumbnailSrc !== '') {
|
||||
$thumbnail = '<div class="banner-thumb-wrap">'
|
||||
. '<img src="' . htmlspecialchars($thumbnailSrc, ENT_QUOTES, 'UTF-8') . '" alt="" '
|
||||
. 'data-preview-src="' . htmlspecialchars($thumbnailSrc, ENT_QUOTES, 'UTF-8') . '" '
|
||||
. 'class="banner-thumb-image js-banner-thumb-preview" '
|
||||
. 'loading="lazy">'
|
||||
. '</div>';
|
||||
}
|
||||
|
||||
$rows[] = [
|
||||
'lp' => $lp++ . '.',
|
||||
'thumbnail' => $thumbnail,
|
||||
'name' => '<a href="/admin/banners/banner_edit/id=' . $id . '">' . htmlspecialchars($name, ENT_QUOTES, 'UTF-8') . '</a>',
|
||||
'status' => $isActive ? 'tak' : '<span style="color: #FF0000;">nie</span>',
|
||||
'home_page' => $homePage === 1 ? '<span class="text-system">tak</span>' : 'nie',
|
||||
'slider' => $homePage === 1 ? 'nie' : '<span class="text-system">tak</span>',
|
||||
'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';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
namespace admin\Controllers;
|
||||
|
||||
class FilemanagerController
|
||||
{
|
||||
private const RFM_KEY_TTL = 1200; // 20 min
|
||||
private const FILEMANAGER_DIALOG_PATH = '/libraries/filemanager-9.14.2/dialog.php';
|
||||
|
||||
public function draw(): string
|
||||
{
|
||||
$akey = $this->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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,165 @@
|
||||
<?php
|
||||
namespace admin\Controllers;
|
||||
|
||||
use Domain\Product\ProductRepository;
|
||||
|
||||
class ProductArchiveController
|
||||
{
|
||||
private ProductRepository $productRepository;
|
||||
|
||||
public function __construct(ProductRepository $productRepository)
|
||||
{
|
||||
$this->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 = '<small class="text-muted product-categories">'
|
||||
. htmlspecialchars($categories, ENT_QUOTES, 'UTF-8')
|
||||
. '</small>';
|
||||
}
|
||||
|
||||
$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 = '<small class="text-muted product-categories">' . implode(', ', $skuEanParts) . '</small>';
|
||||
}
|
||||
|
||||
$productCell = '<div class="product-image product-archive-thumb-wrap">'
|
||||
. '<img src="' . htmlspecialchars($imageSrc, ENT_QUOTES, 'UTF-8') . '" alt="' . htmlspecialchars($imageAlt, ENT_QUOTES, 'UTF-8') . '" '
|
||||
. 'data-preview-src="' . htmlspecialchars($imageSrc, ENT_QUOTES, 'UTF-8') . '" '
|
||||
. 'class="img-responsive product-archive-thumb-image js-product-archive-thumb-preview" loading="lazy">'
|
||||
. '</div>'
|
||||
. '<div class="product-name">'
|
||||
. '<a href="/admin/shop_product/product_edit/id=' . $id . '">' . htmlspecialchars($name, ENT_QUOTES, 'UTF-8') . '</a>'
|
||||
. '</div>'
|
||||
. $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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,356 @@
|
||||
<?php
|
||||
namespace admin;
|
||||
|
||||
class Site
|
||||
{
|
||||
// define APP_SECRET_KEY
|
||||
const APP_SECRET_KEY = 'c3cb2537d25c0efc9e573d059d79c3b8';
|
||||
|
||||
static public function finalize_admin_login( array $user, string $domain, string $cookie_name, bool $remember = false ) {
|
||||
|
||||
\S::set_session( 'user', $user );
|
||||
\S::delete_session( 'twofa_pending' );
|
||||
|
||||
if ( $remember ) {
|
||||
$payloadArr = [
|
||||
'login' => $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() );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,414 @@
|
||||
<?php
|
||||
namespace admin\controls;
|
||||
class ShopProduct
|
||||
{
|
||||
static public function mass_edit_save()
|
||||
{
|
||||
global $mdb;
|
||||
|
||||
if ( \S::get( 'discount_percent' ) != '' and \S::get( 'products' ) )
|
||||
{
|
||||
$product_details = \admin\factory\ShopProduct::product_details( \S::get( 'products' )[0] );
|
||||
|
||||
$vat = $product_details['vat'];
|
||||
$price_brutto = $product_details['price_brutto'];
|
||||
$price_brutto_promo = $price_brutto - ( $price_brutto * ( \S::get( 'discount_percent' ) / 100 ) );
|
||||
$price_netto = $product_details['price_netto'];
|
||||
$price_netto_promo = $price_netto - ( $price_netto * ( \S::get( 'discount_percent' ) / 100 ) );
|
||||
|
||||
if ( $price_brutto == $price_brutto_promo)
|
||||
$price_brutto_promo = null;
|
||||
|
||||
if ( $price_netto == $price_netto_promo )
|
||||
$price_netto_promo = null;
|
||||
|
||||
$mdb -> 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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user