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:
2026-02-11 00:26:01 +01:00
parent 6dd3f7b4d4
commit 336891276d
16 changed files with 2005 additions and 123 deletions

View File

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

View File

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

View File

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

View File

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

View File

@@ -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);
$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`

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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() );
}
}
}
}

View File

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