Refactor code structure for improved readability and maintainability

This commit is contained in:
2026-02-06 01:45:58 +01:00
parent 8e5d0c6854
commit f1c7019cc5
21 changed files with 607 additions and 616 deletions

File diff suppressed because one or more lines are too long

107
DATABASE_STRUCTURE.md Normal file
View File

@@ -0,0 +1,107 @@
# Struktura bazy danych shopPRO
Plik aktualizowany na bieżąco przy zmianach w kodzie.
ORM: Medoo (`$mdb`), prefix tabel: `pp_`
## pp_shop_products
Główna tabela produktów.
| Kolumna | Opis |
|---------|------|
| id | PK |
| parent_id | FK do produktu nadrzędnego (kombinacje) - NULL dla produktów głównych |
| price_brutto | Cena brutto |
| price_brutto_promo | Cena promocyjna brutto |
| quantity | Stan magazynowy |
| status | Status: 1 = aktywny, 0 = nieaktywny |
| archive | Archiwum: 1 = zarchiwizowany, 0 = aktywny |
| promoted | Czy promowany |
| vat | Stawka VAT |
| ean | Kod EAN |
| sku | Kod SKU |
| baselinker_product_name | Nazwa produktu w Baselinker |
| apilo_product_name | Nazwa produktu w Apilo |
**Używane w:** `Domain\Product\ProductRepository`, `admin\factory\ShopProduct`
## pp_shop_products_langs
Tłumaczenia produktów (per język).
| Kolumna | Opis |
|---------|------|
| id | PK |
| product_id | FK do pp_shop_products |
| lang_id | ID języka (np. 'pl') |
| name | Nazwa produktu |
**Używane w:** `Domain\Product\ProductRepository::getName()`
## pp_shop_products_images
Zdjęcia produktów.
| Kolumna | Opis |
|---------|------|
| id | PK |
| product_id | FK do pp_shop_products |
| src | Ścieżka do pliku |
| alt | Tekst alternatywny |
## pp_shop_products_categories
Przypisanie produktów do kategorii.
| Kolumna | Opis |
|---------|------|
| product_id | FK do pp_shop_products |
**Używane w:** `admin\factory\ShopProduct::product_delete()`
## pp_banners
Banery.
| Kolumna | Opis |
|---------|------|
| id | PK |
| name | Nazwa banera |
| status | 0/1 |
| date_start | Data rozpoczęcia |
| date_end | Data zakończenia |
| home_page | Czy na stronie głównej 0/1 |
**Używane w:** `Domain\Banner\BannerRepository`
## pp_banners_langs
Tłumaczenia banerów.
| Kolumna | Opis |
|---------|------|
| id | PK |
| id_banner | FK do pp_banners |
| id_lang | ID języka |
| src | Ścieżka do grafiki |
| url | URL docelowy |
| html | Kod HTML |
| text | Tekst |
**Używane w:** `Domain\Banner\BannerRepository`
## pp_articles
Artykuły.
| Kolumna | Opis |
|---------|------|
| id | PK |
| status | -1 = archiwum, 0 = nieaktywny, 1 = aktywny |
**Używane w:** `admin\controls\ArticlesArchive`
## pp_articles_pages
Strony artykułów.
## pp_articles_langs
Tłumaczenia artykułów.
## pp_articles_images
Zdjęcia artykułów.
## pp_articles_files
Pliki artykułów.

View File

@@ -185,7 +185,7 @@ $product = \shop\Product::getFromCache($product_id, $lang_id, $permutation_hash)
autoload/ autoload/
├── Domain/ # Nowa warstwa biznesowa (namespace \Domain\) ├── Domain/ # Nowa warstwa biznesowa (namespace \Domain\)
│ ├── Product/ │ ├── Product/
│ │ └── ProductRepository.php # getQuantity, getPrice, getName, find, updateQuantity │ │ └── ProductRepository.php # getQuantity, getPrice, getName, find, updateQuantity, archive, unarchive
│ ├── Banner/ │ ├── Banner/
│ │ └── BannerRepository.php # find, delete, save │ │ └── BannerRepository.php # find, delete, save
│ ├── Settings/ │ ├── Settings/
@@ -195,7 +195,8 @@ autoload/
├── admin/ ├── admin/
│ ├── Controllers/ # Nowe kontrolery (namespace \admin\Controllers\) │ ├── Controllers/ # Nowe kontrolery (namespace \admin\Controllers\)
│ │ ├── BannerController.php # DI, instancyjny │ │ ├── BannerController.php # DI, instancyjny
│ │ ── SettingsController.php # DI, instancyjny (clearCache, save, view) │ │ ── SettingsController.php # DI, instancyjny (clearCache, save, view)
│ │ └── ProductArchiveController.php # DI, instancyjny (list, unarchive)
│ ├── class.Site.php # Router: nowy kontroler → fallback stary │ ├── class.Site.php # Router: nowy kontroler → fallback stary
│ ├── controls/ # Stare kontrolery (niezależny fallback) │ ├── controls/ # Stare kontrolery (niezależny fallback)
│ ├── factory/ # Stare helpery (niezależny fallback) │ ├── factory/ # Stare helpery (niezależny fallback)
@@ -240,13 +241,25 @@ tests/
│ │ ├── Settings/SettingsRepositoryTest.php # 3 testy │ │ ├── Settings/SettingsRepositoryTest.php # 3 testy
│ │ └── Cache/CacheRepositoryTest.php # 4 testy │ │ └── Cache/CacheRepositoryTest.php # 4 testy
│ └── admin/ │ └── admin/
│ └── Controllers/SettingsControllerTest.php # 7 testów │ └── Controllers/
│ ├── SettingsControllerTest.php # 7 testów
│ └── ProductArchiveControllerTest.php # 6 testów
└── Integration/ └── Integration/
``` ```
**Łącznie: 29 testów, 60 asercji** **Łącznie: 39 testów, 73 asercji**
## Ostatnie modyfikacje ## Ostatnie modyfikacje
### 2026-02-06: Migracja ProductArchive (ver. 0.241)
- **NOWE:** `admin\Controllers\ProductArchiveController` - kontroler archiwum produktów z DI
- **NOWE:** `ProductRepository::archive()`, `unarchive()` - operacje archiwizacji w repozytorium
- **RENAME:** `admin/templates/archive/``admin/templates/product_archive/`
- **FIX:** SQL w `ajax_products_list_archive()` - puste wyszukiwanie generowało `name|ean|sku LIKE '%%'` (NULL bitwise OR filtrował wyniki)
- **FIX:** Brakujący `archive = 1` w branchu bez wyszukiwania
- **CLEANUP:** Usunięto zbędny JS z szablonu archiwum (apilo, baselinker, duplikowanie, edycja cen)
- Stary kontroler `admin\controls\Archive` zachowany jako fallback
- Testy: 39 testów, 73 asercji (+10 nowych)
### 2026-02-05: Migracja Settings + Cache (ver. 0.240) ### 2026-02-05: Migracja Settings + Cache (ver. 0.240)
- **NOWE:** `Domain\Settings\SettingsRepository` - repozytorium ustawień (fasada → factory) - **NOWE:** `Domain\Settings\SettingsRepository` - repozytorium ustawień (fasada → factory)
- **NOWE:** `Domain\Cache\CacheRepository` - repozytorium cache (dirs + Redis) - **NOWE:** `Domain\Cache\CacheRepository` - repozytorium cache (dirs + Redis)

View File

@@ -13,7 +13,7 @@ Stopniowe przeniesienie logiki biznesowej do architektury warstwowej:
autoload/ autoload/
├── Domain/ # Logika biznesowa (CORE) - namespace \Domain\ ├── Domain/ # Logika biznesowa (CORE) - namespace \Domain\
│ ├── Product/ │ ├── Product/
│ │ ├── ProductRepository.php # ✅ Zmigrowane (getQuantity, getPrice, getName, find, updateQuantity) │ │ ├── ProductRepository.php # ✅ Zmigrowane (getQuantity, getPrice, getName, find, updateQuantity, archive, unarchive)
│ │ ├── ProductService.php # Logika biznesowa (przyszłość) │ │ ├── ProductService.php # Logika biznesowa (przyszłość)
│ │ └── ProductCacheService.php # Cache produktu (przyszłość) │ │ └── ProductCacheService.php # Cache produktu (przyszłość)
│ ├── Banner/ │ ├── Banner/
@@ -155,6 +155,13 @@ grep -r "Product::getQuantity" .
- Testy: ✅ 2 nowe testy (nazwa znaleziona, nie znaleziona) - Testy: ✅ 2 nowe testy (nazwa znaleziona, nie znaleziona)
- Użycie: brak aktywnych wywołań (przygotowane na przyszłość) - Użycie: brak aktywnych wywołań (przygotowane na przyszłość)
- Aktualizacja: ver. 0.239 - Aktualizacja: ver. 0.239
- ✅ archive() / unarchive() - **ZMIGROWANE** (2026-02-06) 🎉
- Nowe metody: `Domain\Product\ProductRepository::archive()`, `unarchive()`
- Nowy kontroler: `admin\Controllers\ProductArchiveController` (DI, instancyjny)
- Szablony: `admin/templates/product_archive/` (rename z `archive/`)
- 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
- [ ] is_product_on_promotion() - NASTĘPNA 👉 - [ ] is_product_on_promotion() - NASTĘPNA 👉
- **Banner** (DEMO pełnej migracji kontrolera) - **Banner** (DEMO pełnej migracji kontrolera)
@@ -200,15 +207,17 @@ composer require --dev phpunit/phpunit
tests/ tests/
├── Unit/ ├── Unit/
│ ├── Domain/ │ ├── Domain/
│ │ ├── Product/ProductRepositoryTest.php # 11 testów │ │ ├── Product/ProductRepositoryTest.php # 15 testów
│ │ ├── Banner/BannerRepositoryTest.php # 4 testy │ │ ├── Banner/BannerRepositoryTest.php # 4 testy
│ │ ├── Settings/SettingsRepositoryTest.php # 3 testy │ │ ├── Settings/SettingsRepositoryTest.php # 3 testy
│ │ └── Cache/CacheRepositoryTest.php # 4 testy │ │ └── Cache/CacheRepositoryTest.php # 4 testy
│ └── admin/ │ └── admin/
│ └── Controllers/SettingsControllerTest.php # 7 testów │ └── Controllers/
│ ├── SettingsControllerTest.php # 7 testów
│ └── ProductArchiveControllerTest.php # 6 testów
└── Integration/ └── Integration/
``` ```
**Łącznie: 29 testów, 60 asercji** **Łącznie: 39 testów, 73 asercji**
### Przykład testu ### Przykład testu
```php ```php
@@ -283,12 +292,14 @@ vendor/bin/phpstan analyse autoload/Domain
- ✅ getQuantity (ver. 0.238) - ✅ getQuantity (ver. 0.238)
- ✅ getPrice (ver. 0.239) - ✅ getPrice (ver. 0.239)
- ✅ getName (ver. 0.239) - ✅ getName (ver. 0.239)
- ✅ archive / unarchive (ver. 0.241)
- [ ] is_product_on_promotion - NASTĘPNA 👉 - [ ] is_product_on_promotion - NASTĘPNA 👉
- [ ] getFromCache - [ ] getFromCache
- [ ] getProductImg - [ ] getProductImg
3. **Banner** ✅ (pełna migracja kontrolera, ver. 0.239) 3. **Banner** ✅ (pełna migracja kontrolera, ver. 0.239)
4. **Settings** ✅ (migracja kontrolera - krok pośredni, ver. 0.240) 4. **Settings** ✅ (migracja kontrolera - krok pośredni, ver. 0.240)
5. **Order** 5. **ProductArchive** ✅ (migracja kontrolera + cleanup szablonów, ver. 0.241)
6. **Order**
5. **Category** 5. **Category**
6. **ShopAttribute** 6. **ShopAttribute**

View File

@@ -14,11 +14,11 @@ Aktualizacje znajdują się w folderze `updates/0.XX/` gdzie XX oznacza dziesią
## Procedura tworzenia nowej aktualizacji ## Procedura tworzenia nowej aktualizacji
### 1. Określ numer wersji ### 1. Określ numer wersji
Sprawdź ostatnią wersję w `updates/0.20/` i zwiększ o 1. Sprawdź ostatnią wersję w `temp/` i zwiększ o 1.
### 2. Utwórz folder tymczasowy ze strukturą ### 2. Utwórz folder tymczasowy ze strukturą w katalogu temp
```bash ```bash
mkdir -p updates/0.20/temp_XXX/sciezka/do/pliku mkdir -p temp/temp_XXX/sciezka/do/pliku
``` ```
**WAŻNE:** W archiwum ZIP NIE powinno być folderu z nazwą wersji (np. ver_0.234/). **WAŻNE:** W archiwum ZIP NIE powinno być folderu z nazwą wersji (np. ver_0.234/).
@@ -26,18 +26,18 @@ Struktura ZIP powinna zaczynać się bezpośrednio od katalogów projektu (admin
### 3. Skopiuj zmienione pliki do folderu tymczasowego ### 3. Skopiuj zmienione pliki do folderu tymczasowego
```bash ```bash
cp sciezka/do/pliku.php updates/0.20/temp_XXX/sciezka/do/pliku.php cp sciezka/do/pliku.php temp/temp_XXX/sciezka/do/pliku.php
``` ```
### 4. Utwórz plik ZIP z zawartości folderu (nie z samego folderu!) ### 4. Utwórz plik ZIP z zawartości folderu (nie z samego folderu!)
```powershell ```powershell
cd updates/0.20/temp_XXX cd temp/temp_XXX
powershell -Command "Compress-Archive -Path '*' -DestinationPath '../ver_X.XXX.zip' -Force" powershell -Command "Compress-Archive -Path '*' -DestinationPath '../ver_X.XXX.zip' -Force"
``` ```
### 5. Usuń folder tymczasowy ### 5. Usuń folder tymczasowy
```bash ```bash
rm -rf updates/0.20/temp_XXX rm -rf temp/temp_XXX
``` ```
### 6. Zaktualizuj changelog.php ### 6. Zaktualizuj changelog.php
@@ -82,15 +82,15 @@ Opis: Dodanie przycisku do zaznaczania zamówienia jako wysłane do trustmate.io
```bash ```bash
# Utwórz strukturę w folderze tymczasowym # Utwórz strukturę w folderze tymczasowym
mkdir -p updates/0.20/temp_234/autoload/admin/controls mkdir -p temp/temp_234/autoload/admin/controls
mkdir -p updates/0.20/temp_234/admin/templates/shop-order mkdir -p temp/temp_234/admin/templates/shop-order
# Skopiuj pliki # Skopiuj pliki
cp autoload/admin/controls/class.ShopOrder.php updates/0.20/temp_234/autoload/admin/controls/ cp autoload/admin/controls/class.ShopOrder.php temp/temp_234/autoload/admin/controls/
cp admin/templates/shop-order/order-details.php updates/0.20/temp_234/admin/templates/shop-order/ cp admin/templates/shop-order/order-details.php temp/temp_234/admin/templates/shop-order/
# Utwórz ZIP z ZAWARTOŚCI folderu (ważne: wejdź do folderu i spakuj '*') # Utwórz ZIP z ZAWARTOŚCI folderu (ważne: wejdź do folderu i spakuj '*')
cd updates/0.20/temp_234 cd temp/temp_234
powershell -Command "Compress-Archive -Path '*' -DestinationPath '../ver_0.234.zip' -Force" powershell -Command "Compress-Archive -Path '*' -DestinationPath '../ver_0.234.zip' -Force"
# Wróć i usuń folder tymczasowy # Wróć i usuń folder tymczasowy

View File

@@ -1,78 +0,0 @@
<? $i = ( $this -> current_page - 1 ) * 10 + 1;?>
<? foreach ( $this -> products as $product ):?>
<tr>
<td>
<?= $i++;?>
</td>
<td>
<div class="product-image">
<? if ( $product['images'][0]['src'] ):?>
<img src="<?= $product['images'][0]['src'];?>" alt="<?= $product['images'][0]['alt'];?>" class="img-responsive">
<? else:?>
<img src="/admin/layout/images/no-image.png" alt="Brak zdjęcia" class="img-responsive">
<? endif;?>
</div>
<div class="product-name">
<a href="/admin/shop_product/product_edit/id=<?= $product['id'];?>">
<?= $product['languages']['pl']['name'];?>
</a>
<a href="#" class="text-muted duplicate-product" product-id="<?= $product['id'];?>">duplikuj</a>
</div>
<small class="text-muted"><?= \admin\factory\ShopProduct::product_categories( $product['id'] );?></small>
</td>
<td class="text-center">
<input type="text" class="product-price form-control text-right" product-id="<?= $product['id'];?>" value="<?= $product['price_brutto'];?>" style="max-width: 75px;">
</td>
<td class="text-center">
<input type="text" class="product-price-promo form-control text-right" product-id="<?= $product['id'];?>" value="<?= $product['price_brutto_promo'];?>" style="max-width: 75px;">
</td>
<td class="text-center">
<?= $product['promoted'] ? '<span class="text-success text-bold">tak</span>' : 'nie';?>
</td>
<td class="text-center">
<?= $product['status'] ? 'tak' : '<span class="text-danger text-bold">nie</span>';?>
</td>
<td class="text-center">
<span class="text-muted"><?= (int)\admin\factory\shopProduct::get_product_quantity_list( $product['id'] );?></span>
</td>
<td class="text-center">
<?
if ( $product['baselinker_product_name'] != "" ) {
echo "<span title='" . htmlspecialchars( $product['baselinker_product_name'] ) . "'>" . mb_substr( $product['baselinker_product_name'], 0, 25, 'UTF-8' ) . "...</span>";
echo "<br>";
echo "<span class='text-danger baselinker-delete-linking' product-id='" . $product['id'] . "'>";
echo "<i class='fa fa-times'></i>usuń powiązanie";
echo "</span>";
} else {
echo "<span class='text-danger baselinker-product-search' product-id='" . $product['id'] . "'>";
echo "nie przypisano <i class='fa fa-search'></i>";
echo "</span>";
}
?>
</td>
<td class="text-center">
<?
if ( $product['apilo_product_name'] != "" ) {
echo "<span title='" . htmlspecialchars( $product['apilo_product_name'] ) . "'>" . mb_substr( $product['apilo_product_name'], 0, 25, "UTF-8" ) . "...</span>";
echo "<br>";
echo "<span class='text-danger apilo-delete-linking' product-id='" . $product['id'] . "'>";
echo "<i class='fa fa-times'></i>usuń powiązanie";
echo "</span>";
} else {
echo "<span class='text-danger apilo-product-search' product-id='" . $product['id'] . "'>";
echo "nie przypisano <i class='fa fa-search'></i>";
echo "</span>";
}
?>
</td>
<td>
<a href='/admin/shop_product/product_combination/product_id=<?= $product['id'];?>'>kombinacje (<?= \admin\factory\shopProduct::count_product_combinations( $product['id'] );?>)</a>
</td>
<td class="text-center">
<a href='/admin/shop_product/product_edit/id=<?= $product['id'];?>'>edytuj</a>
</td>
<td class="text-center">
<a href='/admin/shop_product/product_unarchive/product_id=<?= $product['id'];?>' class="product-unarchive">przywróć</a>
</td>
</tr>
<? endforeach;?>

View File

@@ -1,488 +0,0 @@
<div class="panel">
<div class="panel-body">
<a href="/admin/shop_product/product_edit/" class="btn btn-success select-all">Dodaj produkt</a>
</div>
<div class="panel-body pn">
<div class="table-responsive">
<table class="table table-bordered table-hover table-striped mbn" id="table-products">
<thead>
<tr>
<th style="width: 10px;">#</th>
<th>Nazwa</th>
<th class="text-center" style="width: 100px;">Cena</th>
<th class="text-center" style="width: 100px;">Cena promocyjna</th>
<th class="text-center" style="width: 25px;">Promowany</th>
<th class="text-center" style="width: 25px;">Aktywny</th>
<th class="text-center" style="width: 75px;">Stan MG</th>
<th class="text-center" style="width: 100px;">Baselinker</th>
<th class="text-center" style="width: 100px;">Apilo</th>
<th class="text-center" style="width: 100px;">Kombinacje</th>
<th class="text-center" style="width: 75px;">Edytuj</th>
<th class="text-center" style="width: 75px;">Przywróć</th>
</tr>
<tr>
<th></th>
<th>
<input type="text" class="form-control table-search" field_name="name|ean|sku" placeholder="szukaj..." value="<?= htmlspecialchars( $this -> query_array['name'], ENT_QUOTES, 'UTF-8');?>">
</th>
<th colspan="10"></th>
</tr>
</thead>
<tbody>
</tbody>
<tfoot>
<tr>
<td colspan="12">
<ul class="pagination" pagination_max="<?= $this -> pagination_max;?>">
<li>
<a href="#" page="1" title="pierwsza strona">
<i class="fa fa-angle-double-left"></i>
</a>
</li>
<li>
<a href="#" class="previous" page="<?= ( $this -> current_page - 1 > 1 ) ? ( $this -> current_page - 1 ) : 1;?>" title="poprzednia strona">
<i class="fa fa-angle-left" title="poprzednia strona"></i>
</a>
</li>
<li>
<div>
Strona <span id="current-page"><?= $this -> current_page;?></span> z <span id="max_page"><?= $this -> pagination_max;?></span>
</div>
</li>
<li>
<a href="#" class="next" page="<?= ( $this -> current_page + 1 < $this -> pagination_max ) ? ( $this -> current_page + 1 ) : $this -> pagination_max;?>" title="następna strona">
<i class="fa fa-angle-right"></i>
</a>
</li>
<li>
<a href="#" class="last" page="<?= $this -> pagination_max;?>" title="ostatnia strona">
<i class="fa fa-angle-double-right"></i>
</a>
</li>
</ul>
</td>
</tr>
</tfoot>
</table>
</div>
</div>
</div>
<script type="text/javascript">
$( function() {
ajax_load_products( <?= $this -> current_page;?>, null );
$( 'body' ).on( 'change', '.table-search', function() {
ajax_load_products( 1 );
});
$( 'body' ).on( 'click', '.pagination a', function() {
var current_page = $( this ).attr( 'page' );
ajax_load_products( current_page );
});
$( 'body' ).on( 'click', '.product-unarchive', function(e) {
e.preventDefault();
var href = $( this ).attr( 'href' );
$.alert({
title: 'Pytanie',
content: 'Na pewno chcesz przywrócić wybrany produkt z archiwum?',
type: 'orange',
closeIcon: true,
closeIconClass: 'fa fa-times',
typeAnimated: true,
animation: 'opacity',
columnClass: 'col-12 col-lg-10',
theme: 'supervan',
icon: 'fa fa-question',
buttons: {
cancel: {
text: 'Nie',
btnClass: 'btn-success',
action: function() {}
},
confirm: {
text: 'Tak',
btnClass: 'btn-danger',
keys: ['enter'],
action: function() {
document.location.href = href;
}
}
}
});
return false;
});
$( 'body' ).on( 'change', '.product-price-promo', function(e)
{
var price = $( this ).val();
price = price.replace( ' ', '' );
price = parseFloat( price.replace( ',', '.' ) * 1 );
price = number_format( price, 2, '.', '' );
$( this ).val( price );
var product_id = $( this ).attr( 'product-id' );
$.ajax(
{
type: 'POST',
cache: false,
url: '/admin/shop_product/product_change_price_brutto_promo/',
data:
{
product_id: product_id,
price: price
},
beforeSend: function()
{
$( '#overlay' ).show();
},
success: function( data )
{
$( '#overlay' ).hide();
response = jQuery.parseJSON( data );
if ( response.status !== 'ok' )
create_error( response.msg );
}
});
});
$( 'body' ).on( 'change', '.product-price', function(e)
{
var price = $( this ).val();
price = price.replace( ' ', '' );
price = parseFloat( price.replace( ',', '.' ) * 1 );
price = number_format( price, 2, '.', '' );
$( this ).val( price );
var product_id = $( this ).attr( 'product-id' );
$.ajax(
{
type: 'POST',
cache: false,
url: '/admin/shop_product/product_change_price_brutto/',
data:
{
product_id: product_id,
price: price
},
beforeSend: function()
{
$( '#overlay' ).show();
},
success: function( data )
{
$( '#overlay' ).hide();
response = jQuery.parseJSON( data );
if ( response.status !== 'ok' )
create_error( response.msg );
}
});
});
$( 'body' ).on( 'click', '.duplicate-product', function(e)
{
e.preventDefault();
var product_id = $( this ).attr( 'product-id' );
$.alert({
title: 'Pytanie',
content: 'Na pewno chcesz wykonać duplikat produktu?',
type: 'orange',
closeIcon: true,
closeIconClass: 'fa fa-times',
typeAnimated: true,
animation: 'opacity',
columnClass: 'col-12 col-lg-10',
theme: 'modern',
icon: 'fa fa-question',
buttons: {
confirm: {
text: 'Tak (produkt bez kombinacji)',
btnClass: 'btn-success',
keys: ['enter'],
action: function() {
document.location.href = '/admin/shop_product/duplicate_product/product-id=' + product_id;
}
},
confirm2: {
text: 'Tak (produkt z KOMBINACJAMI)',
btnClass: 'btn-primary',
keys: ['enter'],
action: function() {
document.location.href = '/admin/shop_product/duplicate_product/product-id=' + product_id + '&combination=1';
}
},
cancel: {
text: 'Nie',
btnClass: 'btn-dark',
action: function() {}
}
}
});
})
// apilo product search
$( 'body' ).on( 'click', '.apilo-product-search', function() {
var product_id = $( this ).attr( 'product-id' );
$.ajax({
type: 'POST',
cache: false,
url: '/admin/integrations/apilo_product_search/',
data: {
product_id: product_id
},
beforeSend: function() {
$( '#overlay' ).show();
},
success: function( response ) {
data = jQuery.parseJSON( response );
if ( data.status == 'SUCCESS' ) {
var html = '<div class="apilo-found-products">';
html += '<p>Znaleziono ' + data.products.length + ' produktów</p>';
html += '<select class="form-control apilo-product-select" product-id="' + product_id + '">';
$.each( data.products, function( index, value ) {
html += '<option value="' + value.id + '">' + value.name + ' SKU: ' + value.sku + '</option>';
});
html += '</select>';
html += '<button class="btn btn-success apilo-product-select-save" product-id="' + product_id + '">Zapisz</button>';
html += '</div>';
$( 'span.apilo-product-search[product-id="' + product_id + '"]' ).closest( 'td' ).append( html );
} else if ( data.status == 'error' ) {
$.alert({
title: 'Błąd',
content: data.msg,
type: 'red',
closeIcon: true,
closeIconClass: 'fa fa-times',
typeAnimated: true,
animation: 'opacity',
columnClass: 'col-12 col-lg-10',
theme: 'modern',
icon: 'fa fa-exclamation-triangle',
buttons: {
confirm: {
text: 'OK',
btnClass: 'btn-danger',
keys: ['enter'],
action: function() {}
}
}
});
}
}
});
});
// delete apilo product linking
$( 'body' ).on( 'click', '.apilo-delete-linking', function() {
var product_id = $( this ).attr( 'product-id' );
$.ajax({
type: 'POST',
cache: false,
url: '/admin/shop_product/apilo_product_select_delete/',
data: {
product_id: product_id
},
beforeSend: function() {
$( '#overlay' ).show();
},
success: function( response ) {
data = jQuery.parseJSON( response );
if ( data.status == 'ok' ) {
$( 'span.apilo-delete-linking[product-id="' + product_id + '"]' ).closest('td').html( '<span class="text-danger apilo-product-search" product-id="' + product_id + '">nie przypisano <i class="fa fa-search"></i></span>' );
}
}
});
});
// delete baselinker product linking
$( 'body' ).on( 'click', '.baselinker-delete-linking', function() {
var product_id = $( this ).attr( 'product-id' );
$.ajax({
type: 'POST',
cache: false,
url: '/admin/shop_product/baselinker_product_select_delete/',
data: {
product_id: product_id
},
beforeSend: function() {
$( '#overlay' ).show();
},
success: function( response ) {
data = jQuery.parseJSON( response );
if ( data.status == 'ok' ) {
$( 'span.baselinker-delete-linking[product-id="' + product_id + '"]' ).closest('td').html( '<span class="text-danger baselinker-product-search" product-id="' + product_id + '">nie przypisano <i class="fa fa-search"></i></span>' );
}
}
});
});
$( 'body' ).on( 'click', '.baselinker-product-search', function() {
var product_id = $( this ).attr( 'product-id' );
$.ajax({
type: 'POST',
cache: false,
url: '/admin/shop_product/baselinker_product_search/',
data: {
product_id: product_id
},
beforeSend: function() {
$( '#overlay' ).show();
},
success: function( response ) {
data = jQuery.parseJSON( response );
if ( data.status == 'SUCCESS' ) {
var html = '<div class="baselinker-found-products">';
html += '<p>Znaleziono ' + data.products.length + ' produktów</p>';
html += '<select class="form-control baselinker-product-select" product-id="' + product_id + '">';
$.each( data.products, function( index, value ) {
html += '<option value="' + value.product_id + '">' + value.name + ' SKU: ' + value.sku + '</option>';
});
html += '</select>';
html += '<button class="btn btn-success baselinker-product-select-save" product-id="' + product_id + '">Zapisz</button>';
html += '</div>';
$( 'span.baselinker-product-search[product-id="' + product_id + '"]' ).closest( 'td' ).append( html );
} else if ( data.status == 'error' ) {
$.alert({
title: 'Błąd',
content: data.msg,
type: 'red',
closeIcon: true,
closeIconClass: 'fa fa-times',
typeAnimated: true,
animation: 'opacity',
columnClass: 'col-12 col-lg-10',
theme: 'modern',
icon: 'fa fa-exclamation-triangle',
buttons: {
confirm: {
text: 'OK',
btnClass: 'btn-danger',
keys: ['enter'],
action: function() {}
}
}
});
}
}
});
});
// apilo product select save
$( 'body' ).on( 'click', '.apilo-product-select-save', function(){
var product_id = $( this ).attr( 'product-id' );
var apilo_product_id = $( '.apilo-product-select[product-id="' + product_id + '"]' ).val();
var apilo_product_name = $( '.apilo-product-select[product-id="' + product_id + '"] option:selected' ).text();
$.ajax({
type: 'POST',
cache: false,
url: '/admin/shop_product/apilo_product_select_save/',
data: {
product_id: product_id,
apilo_product_id: apilo_product_id,
apilo_product_name: apilo_product_name
},
beforeSend: function() {
$( '#overlay' ).show();
},
success: function( response ) {
data = jQuery.parseJSON( response );
if ( data.status == 'ok' ) {
$( '.apilo-product-select[product-id="' + product_id + '"]' ).closest( '.apilo-found-products' ).remove();
$( 'span.apilo-product-search[product-id="' + product_id + '"]' ).html( '<span title="' + apilo_product_name + '">' + apilo_product_name.substr( 0, 25 ) + '...</span>' ).removeClass( 'apilo-product-search' ).removeClass( 'text-danger' );
}
}
});
});
$( 'body' ).on( 'click', '.baselinker-product-select-save', function(){
var product_id = $( this ).attr( 'product-id' );
var baselinker_product_id = $( '.baselinker-product-select[product-id="' + product_id + '"]' ).val();
var baselinker_product_name = $( '.baselinker-product-select[product-id="' + product_id + '"] option:selected' ).text();
$.ajax({
type: 'POST',
cache: false,
url: '/admin/shop_product/baselinker_product_select_save/',
data: {
product_id: product_id,
baselinker_product_id: baselinker_product_id,
baselinker_product_name: baselinker_product_name
},
beforeSend: function() {
$( '#overlay' ).show();
},
success: function( response ) {
data = jQuery.parseJSON( response );
if ( data.status == 'ok' ) {
$( '.baselinker-product-select[product-id="' + product_id + '"]' ).closest( '.baselinker-found-products' ).remove();
$( 'span.baselinker-product-search[product-id="' + product_id + '"]' ).html( '<span title="' + baselinker_product_name + '">' + baselinker_product_name.substr( 0, 25 ) + '...</span>' ).removeClass( 'baselinker-product-search' ).removeClass( 'text-danger' );
}
}
});
});
});
function ajax_load_products( current_page ) {
var pagination_max = parseInt( $( '.pagination' ).attr( 'pagination_max' ) );
var query = '';
$( '.table-search' ).each(function(){
query += $( this ).attr( 'field_name' ) + '=' + $( this ).val() + '&';
});
current_page = parseInt( current_page );
$.ajax({
type: 'POST',
cache: false,
url: '/admin/shop_product/ajax_load_products_archive/',
data: {
current_page: current_page,
query: query
},
beforeSend: function() {
$( '#overlay' ).show();
},
success: function( response ) {
$( '#overlay' ).hide();
data = jQuery.parseJSON( response );
if ( data.status == 'ok' ) {
$( '#table-products tbody' ).html( data.html );
$( '.pagination .previous' ).attr( 'page', ( current_page - 1 > 1 ) ? ( current_page - 1 ) : 1 );
$( '.pagination .next' ).attr( 'page', ( current_page + 1 < pagination_max ) ? ( current_page + 1 ) : pagination_max );
$( '.pagination span' ).html( current_page );
if ( data.pagination_max ) {
$( '.pagination' ).attr( 'pagination_max', data.pagination_max );
$( '.pagination #max_page' ).html( data.pagination_max );
$( '.pagination .last' ).attr( 'page', data.pagination_max );
}
}
}
});
}
</script>

View File

@@ -0,0 +1,41 @@
<? $i = ( $this -> current_page - 1 ) * 10 + 1;?>
<? foreach ( $this -> products as $product ):?>
<tr>
<td>
<?= $i++;?>
</td>
<td>
<div class="product-image">
<? if ( $product['images'][0]['src'] ):?>
<img src="<?= $product['images'][0]['src'];?>" alt="<?= $product['images'][0]['alt'];?>" class="img-responsive">
<? else:?>
<img src="/admin/layout/images/no-image.png" alt="Brak zdjęcia" class="img-responsive">
<? endif;?>
</div>
<div class="product-name">
<a href="/admin/shop_product/product_edit/id=<?= $product['id'];?>">
<?= $product['languages']['pl']['name'];?>
</a>
</div>
<small class="text-muted"><?= \admin\factory\ShopProduct::product_categories( $product['id'] );?></small>
</td>
<td class="text-center">
<?= $product['price_brutto'];?>
</td>
<td class="text-center">
<?= $product['price_brutto_promo'];?>
</td>
<td class="text-center">
<span class="text-muted"><?= (int)\admin\factory\shopProduct::get_product_quantity_list( $product['id'] );?></span>
</td>
<td>
<a href='/admin/shop_product/product_combination/product_id=<?= $product['id'];?>'>kombinacje (<?= \admin\factory\shopProduct::count_product_combinations( $product['id'] );?>)</a>
</td>
<td class="text-center">
<a href='/admin/shop_product/product_edit/id=<?= $product['id'];?>'>edytuj</a>
</td>
<td class="text-center">
<a href='/admin/product_archive/unarchive/product_id=<?= $product['id'];?>' class="product-unarchive">przywróć</a>
</td>
</tr>
<? endforeach;?>

View File

@@ -0,0 +1,152 @@
<div class="panel">
<div class="panel-body pn">
<div class="table-responsive">
<table class="table table-bordered table-hover table-striped mbn" id="table-products">
<thead>
<tr>
<th style="width: 10px;">#</th>
<th>Nazwa</th>
<th class="text-center" style="width: 100px;">Cena</th>
<th class="text-center" style="width: 100px;">Cena promocyjna</th>
<th class="text-center" style="width: 75px;">Stan MG</th>
<th class="text-center" style="width: 100px;">Kombinacje</th>
<th class="text-center" style="width: 75px;">Edytuj</th>
<th class="text-center" style="width: 75px;">Przywróć</th>
</tr>
<tr>
<th></th>
<th>
<input type="text" class="form-control table-search" field_name="name|ean|sku" placeholder="szukaj..." value="<?= htmlspecialchars( $this -> query_array['name'] ?? '', ENT_QUOTES, 'UTF-8');?>">
</th>
<th colspan="6"></th>
</tr>
</thead>
<tbody>
</tbody>
<tfoot>
<tr>
<td colspan="8">
<ul class="pagination" pagination_max="<?= $this -> pagination_max;?>">
<li>
<a href="#" page="1" title="pierwsza strona">
<i class="fa fa-angle-double-left"></i>
</a>
</li>
<li>
<a href="#" class="previous" page="<?= ( $this -> current_page - 1 > 1 ) ? ( $this -> current_page - 1 ) : 1;?>" title="poprzednia strona">
<i class="fa fa-angle-left" title="poprzednia strona"></i>
</a>
</li>
<li>
<div>
Strona <span id="current-page"><?= $this -> current_page;?></span> z <span id="max_page"><?= $this -> pagination_max;?></span>
</div>
</li>
<li>
<a href="#" class="next" page="<?= ( $this -> current_page + 1 < $this -> pagination_max ) ? ( $this -> current_page + 1 ) : $this -> pagination_max;?>" title="następna strona">
<i class="fa fa-angle-right"></i>
</a>
</li>
<li>
<a href="#" class="last" page="<?= $this -> pagination_max;?>" title="ostatnia strona">
<i class="fa fa-angle-double-right"></i>
</a>
</li>
</ul>
</td>
</tr>
</tfoot>
</table>
</div>
</div>
</div>
<script type="text/javascript">
$( function() {
ajax_load_products( <?= $this -> current_page;?> );
$( 'body' ).on( 'change', '.table-search', function() {
ajax_load_products( 1 );
});
$( 'body' ).on( 'click', '.pagination a', function() {
var current_page = $( this ).attr( 'page' );
ajax_load_products( current_page );
});
$( 'body' ).on( 'click', '.product-unarchive', function(e) {
e.preventDefault();
var href = $( this ).attr( 'href' );
$.alert({
title: 'Pytanie',
content: 'Na pewno chcesz przywrócić wybrany produkt z archiwum?',
type: 'orange',
closeIcon: true,
closeIconClass: 'fa fa-times',
typeAnimated: true,
animation: 'opacity',
columnClass: 'col-12 col-lg-10',
theme: 'supervan',
icon: 'fa fa-question',
buttons: {
cancel: {
text: 'Nie',
btnClass: 'btn-success',
action: function() {}
},
confirm: {
text: 'Tak',
btnClass: 'btn-danger',
keys: ['enter'],
action: function() {
document.location.href = href;
}
}
}
});
return false;
});
});
function ajax_load_products( current_page ) {
var pagination_max = parseInt( $( '.pagination' ).attr( 'pagination_max' ) );
var query = '';
$( '.table-search' ).each(function(){
query += $( this ).attr( 'field_name' ) + '=' + $( this ).val() + '&';
});
current_page = parseInt( current_page );
$.ajax({
type: 'POST',
cache: false,
url: '/admin/shop_product/ajax_load_products_archive/',
data: {
current_page: current_page,
query: query
},
beforeSend: function() {
$( '#overlay' ).show();
},
success: function( response ) {
$( '#overlay' ).hide();
data = jQuery.parseJSON( response );
if ( data.status == 'ok' ) {
$( '#table-products tbody' ).html( data.html );
$( '.pagination .previous' ).attr( 'page', ( current_page - 1 > 1 ) ? ( current_page - 1 ) : 1 );
$( '.pagination .next' ).attr( 'page', ( current_page + 1 < pagination_max ) ? ( current_page + 1 ) : pagination_max );
$( '.pagination span' ).html( current_page );
if ( data.pagination_max ) {
$( '.pagination' ).attr( 'pagination_max', data.pagination_max );
$( '.pagination #max_page' ).html( data.pagination_max );
$( '.pagination .last' ).attr( 'page', data.pagination_max );
}
}
}
});
}
</script>

View File

@@ -127,7 +127,7 @@
</div> </div>
<ul> <ul>
<li> <li>
<a href="/admin/archive/products_list/"> <a href="/admin/product_archive/products_list/">
<i class="fa fa-trash" aria-hidden="true"></i>Produkty <i class="fa fa-trash" aria-hidden="true"></i>Produkty
</a> </a>
</li> </li>

View File

@@ -102,4 +102,32 @@ class ProductRepository
return $result !== false; 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,47 @@
<?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
{
$current_page = \S::get_session( 'archive_products_list_current_page' );
if ( !$current_page ) {
$current_page = 1;
\S::set_session( 'archive_products_list_current_page', $current_page );
}
$query = \S::get_session( 'archive_products_list_query' );
$query_array = [];
if ( $query ) {
parse_str( $query, $query_array );
}
return \Tpl::view( 'product_archive/products-list', [
'current_page' => $current_page,
'query_array' => $query_array,
'pagination_max' => ceil( \admin\factory\ShopProduct::count_product( [ 'archive' => 1 ] ) / 10 )
] );
}
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

@@ -211,6 +211,13 @@ class Site
new \Domain\Settings\SettingsRepository() new \Domain\Settings\SettingsRepository()
); );
}, },
'ProductArchive' => function() {
global $mdb;
return new \admin\Controllers\ProductArchiveController(
new \Domain\Product\ProductRepository( $mdb )
);
},
]; ];
return self::$newControllers; return self::$newControllers;
@@ -246,6 +253,7 @@ class Site
'clear_cache' => 'clearCache', 'clear_cache' => 'clearCache',
'clear_cache_ajax' => 'clearCacheAjax', 'clear_cache_ajax' => 'clearCacheAjax',
'settings_save' => 'save', 'settings_save' => 'save',
'products_list' => 'list',
]; ];
public static function route() public static function route()

View File

@@ -1,5 +1,10 @@
<? <?
namespace admin\controls; namespace admin\controls;
/**
* @deprecated Użyj \admin\Controllers\ProductArchiveController
* Klasa zachowana jako fallback dla starego URL /admin/archive/products_list/
*/
class Archive { class Archive {
static public function products_list() { static public function products_list() {
@@ -16,7 +21,7 @@ class Archive {
parse_str( $query, $query_array ); parse_str( $query, $query_array );
} }
return \Tpl::view( 'archive/products-list', [ return \Tpl::view( 'product_archive/products-list', [
'current_page' => $current_page, 'current_page' => $current_page,
'query_array' => $query_array, 'query_array' => $query_array,
'pagination_max' => ceil( \admin\factory\ShopProduct::count_product() / 10 ) 'pagination_max' => ceil( \admin\factory\ShopProduct::count_product() / 10 )

View File

@@ -203,7 +203,7 @@ class ShopProduct
else else
\S::alert( 'Podczas przywracania produktu z archiwum wystąpił błąd. Proszę spróbować ponownie' ); \S::alert( 'Podczas przywracania produktu z archiwum wystąpił błąd. Proszę spróbować ponownie' );
header( 'Location: /admin/archive/products_list/' ); header( 'Location: /admin/product_archive/products_list/' );
exit; exit;
} }
@@ -266,7 +266,7 @@ class ShopProduct
$response = [ $response = [
'status' => 'ok', 'status' => 'ok',
'pagination_max' => ceil( $products['products_count'] / 10 ), 'pagination_max' => ceil( $products['products_count'] / 10 ),
'html' => \Tpl::view( 'archive/products-list-table', [ 'html' => \Tpl::view( 'product_archive/products-list-table', [
'products' => $products['products'], 'products' => $products['products'],
'current_page' => \S::get( 'current_page' ), 'current_page' => \S::get( 'current_page' ),
] ) ] )

View File

@@ -482,16 +482,18 @@ class ShopProduct
{ {
global $mdb; global $mdb;
$search = '';
if ( $query ) if ( $query )
{ {
$search = '';
$query_array = []; $query_array = [];
parse_str( $query, $query_array ); parse_str( $query, $query_array );
foreach ( $query_array as $key => $val ) { foreach ( $query_array as $key => $val ) {
if ( $val !== '' )
$search .= ' AND ' . $key . ' LIKE \'%' . $val . '%\''; $search .= ' AND ' . $key . ' LIKE \'%' . $val . '%\'';
} }
}
$results = $mdb -> query( 'SELECT ' $results = $mdb -> query( 'SELECT '
. 'DISTINCT( psp.id )' . 'DISTINCT( psp.id )'
@@ -505,10 +507,6 @@ class ShopProduct
. 'pp_shop_products AS psp ' . 'pp_shop_products AS psp '
. 'INNER JOIN pp_shop_products_langs AS pspl ON pspl.product_id = psp.id ' . 'INNER JOIN pp_shop_products_langs AS pspl ON pspl.product_id = psp.id '
. 'WHERE archive = 1 AND parent_id IS NULL ' . $search ) -> fetchAll( \PDO::FETCH_ASSOC ); . 'WHERE archive = 1 AND parent_id IS NULL ' . $search ) -> fetchAll( \PDO::FETCH_ASSOC );
} else {
$results = $mdb -> query( 'SELECT id FROM pp_shop_products WHERE parent_id IS NULL ORDER BY id DESC LIMIT ' . ( $current_page - 1 ) * 10 . ', 10' ) -> fetchAll( \PDO::FETCH_ASSOC );
$results2 = $mdb -> query( 'SELECT COUNT( id ) AS products_count FROM pp_shop_products WHERE parent_id IS NULL' ) -> fetchAll( \PDO::FETCH_ASSOC );
}
if ( is_array( $results ) ) foreach ( $results as $row ) { if ( is_array( $results ) ) foreach ( $results as $row ) {
$products[] = \admin\factory\ShopProduct::product_details( $row['id'] ); $products[] = \admin\factory\ShopProduct::product_details( $row['id'] );

View File

@@ -259,4 +259,92 @@ class ProductRepositoryTest extends TestCase
$this->assertIsInt($quantity); // Sprawdzamy czy konwersja na int zadziałała $this->assertIsInt($quantity); // Sprawdzamy czy konwersja na int zadziałała
$this->assertEquals(25, $quantity); $this->assertEquals(25, $quantity);
} }
/**
* Test przywracania produktu z archiwum
*/
public function testUnarchiveUpdatesProductAndChildren()
{
// Arrange
$mockDb = $this->createMock(\medoo::class);
$mockDb->expects($this->exactly(2))
->method('update')
->withConsecutive(
[
$this->equalTo('pp_shop_products'),
$this->equalTo(['status' => 1, 'archive' => 0]),
$this->equalTo(['id' => 123])
],
[
$this->equalTo('pp_shop_products'),
$this->equalTo(['status' => 1, 'archive' => 0]),
$this->equalTo(['parent_id' => 123])
]
);
$repository = new ProductRepository($mockDb);
// Act
$result = $repository->unarchive(123);
// Assert
$this->assertTrue($result);
}
/**
* Test przenoszenia produktu do archiwum
*/
public function testArchiveUpdatesProductAndChildren()
{
// Arrange
$mockDb = $this->createMock(\medoo::class);
$mockDb->expects($this->exactly(2))
->method('update')
->withConsecutive(
[
$this->equalTo('pp_shop_products'),
$this->equalTo(['status' => 0, 'archive' => 1]),
$this->equalTo(['id' => 456])
],
[
$this->equalTo('pp_shop_products'),
$this->equalTo(['status' => 0, 'archive' => 1]),
$this->equalTo(['parent_id' => 456])
]
);
$repository = new ProductRepository($mockDb);
// Act
$result = $repository->archive(456);
// Assert
$this->assertTrue($result);
}
/**
* Test że unarchive zwraca bool
*/
public function testUnarchiveReturnsBool()
{
$mockDb = $this->createMock(\medoo::class);
$repository = new ProductRepository($mockDb);
$result = $repository->unarchive(1);
$this->assertIsBool($result);
}
/**
* Test że archive zwraca bool
*/
public function testArchiveReturnsBool()
{
$mockDb = $this->createMock(\medoo::class);
$repository = new ProductRepository($mockDb);
$result = $repository->archive(1);
$this->assertIsBool($result);
}
} }

View File

@@ -0,0 +1,56 @@
<?php
namespace Tests\Unit\admin\Controllers;
use PHPUnit\Framework\TestCase;
use admin\Controllers\ProductArchiveController;
use Domain\Product\ProductRepository;
class ProductArchiveControllerTest extends TestCase
{
private $mockRepository;
private $controller;
protected function setUp(): void
{
$this->mockRepository = $this->createMock(ProductRepository::class);
$this->controller = new ProductArchiveController($this->mockRepository);
}
public function testConstructorAcceptsRepository(): void
{
$controller = new ProductArchiveController($this->mockRepository);
$this->assertInstanceOf(ProductArchiveController::class, $controller);
}
public function testHasListMethod(): void
{
$this->assertTrue(method_exists($this->controller, 'list'));
}
public function testHasUnarchiveMethod(): void
{
$this->assertTrue(method_exists($this->controller, 'unarchive'));
}
public function testListMethodReturnType(): void
{
$reflection = new \ReflectionClass($this->controller);
$this->assertEquals('string', (string)$reflection->getMethod('list')->getReturnType());
}
public function testUnarchiveMethodReturnType(): void
{
$reflection = new \ReflectionClass($this->controller);
$this->assertEquals('void', (string)$reflection->getMethod('unarchive')->getReturnType());
}
public function testConstructorRequiresProductRepository(): void
{
$reflection = new \ReflectionClass(ProductArchiveController::class);
$constructor = $reflection->getConstructor();
$params = $constructor->getParameters();
$this->assertCount(1, $params);
$this->assertEquals('Domain\Product\ProductRepository', $params[0]->getType()->getName());
}
}

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

Binary file not shown.

View File

@@ -1,3 +1,10 @@
<b>ver. 0.241</b><br />
- NEW - refaktoryzacja: admin\Controllers\ProductArchiveController - archiwum produktów z DI
- NEW - ProductRepository::archive(), unarchive() - operacje archiwizacji w repozytorium
- FIX - naprawiono SQL w liście archiwum (puste wyszukiwanie filtrowało wszystkie wyniki)
- FIX - naprawiono brakujący filtr archive = 1 w zapytaniu bez wyszukiwania
- UPDATE - wyczyszczono szablony archiwum (usunięto zbędne funkcje: apilo, baselinker, duplikowanie)
<hr>
<b>ver. 0.240</b><br /> <b>ver. 0.240</b><br />
- NEW - refaktoryzacja: Domain\Settings\SettingsRepository + admin\Controllers\SettingsController (architektura Domain-Driven) - NEW - refaktoryzacja: Domain\Settings\SettingsRepository + admin\Controllers\SettingsController (architektura Domain-Driven)
- NEW - refaktoryzacja: Domain\Cache\CacheRepository - czyszczenie cache z obsługą Redis - NEW - refaktoryzacja: Domain\Cache\CacheRepository - czyszczenie cache z obsługą Redis

View File

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