refactor(shop_category): migrate admin module to Domain+DI with routing and ajax cleanup

This commit is contained in:
2026-02-15 15:32:23 +01:00
parent e17875526d
commit 6c87e4615a
63 changed files with 8998 additions and 625 deletions

View File

@@ -0,0 +1,3 @@
autoload/admin/controls/class.ShopCategory.php
autoload/admin/factory/class.ShopCategory.php
autoload/admin/view/class.ShopCategory.php

View File

@@ -0,0 +1,15 @@
<?php
$a = \S::get( 'a' );
if ( $a == 'save_products_order' )
{
$response = [ 'status' => 'error', 'msg' => 'Podczas zapisywania kolejności wyświetlania produktów wystąpił błąd. Proszę spróbować ponownie.' ];
$categoryRepository = new \Domain\Category\CategoryRepository( $mdb );
if ( $categoryRepository->saveProductOrder( \S::get( 'category_id' ), \S::get( 'products' ) ) )
$response = [ 'status' => 'ok' ];
echo json_encode( $response );
exit;
}

View File

@@ -0,0 +1,60 @@
<?php
if ( $a == 'cookie_categories' )
{
$array = unserialize( $_COOKIE[ 'cookie_categories' ] );
if ( $array[ \S::get( 'category_id' ) ] == 0 )
$array[ \S::get( 'category_id' ) ] = 1;
else
$array[ \S::get( 'category_id' ) ] = 0;
$array = serialize( $array );
setcookie( 'cookie_categories', $array, time() + 3600 * 24 * 365 );
}
if ( $a == 'save_categories_order' )
{
$response = [ 'status' => 'error', 'msg' => 'Podczas zapisywania kolejności kategorii wystąpił błąd. Proszę spróbować ponownie.' ];
$categoryRepository = new \Domain\Category\CategoryRepository( $mdb );
if ( $categoryRepository->saveCategoriesOrder( \S::get( 'categories' ) ) )
$response = [ 'status' => 'ok' ];
echo json_encode( $response );
exit;
}
if ( $a == 'product_file_delete' )
{
$response = [ 'status' => 'error', 'msg' => 'Podczas usuwania załącznika wystąpił błąd. Proszę spróbować ponownie.' ];
if ( \admin\factory\ShopProduct::delete_file( \S::get( 'file_id' ) ) )
$response = [ 'status' => 'ok' ];
echo json_encode( $response );
exit;
}
if ( $a == 'product_file_name_change' )
{
$response = [ 'status' => 'error', 'msg' => 'Podczas zmiany nazwy załącznika wystąpił błąd. Proszę spróbować ponownie.' ];
if ( \admin\factory\ShopProduct::file_name_change( \S::get( 'file_id' ), \S::get( 'file_name' ) ) )
$response = [ 'status' => 'ok' ];
echo json_encode( $response );
exit;
}
if ( $a == 'product_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;
}

View File

@@ -0,0 +1,153 @@
<style type="text/css">
.layout-tree-toggle {
width: 18px;
height: 18px;
display: inline-flex;
align-items: center;
justify-content: center;
border: 0;
background: transparent;
padding: 0;
margin-right: 4px;
margin-top: -2px;
color: #666;
cursor: pointer;
vertical-align: middle;
}
.layout-tree-toggle i.fa.fa-caret-right {
position: relative;
top: -11px;
line-height: 1;
}
.layout-tree-toggle:focus,
.layout-tree-toggle:active,
.layout-tree-toggle:focus-visible {
outline: none;
box-shadow: none;
}
li.sort-expanded > div .layout-tree-toggle i {
transform: rotate(90deg);
}
</style>
<script type="text/javascript" src="/libraries/jquery-nested-sortable/jquery.mjs.nestedSortable.js"></script>
<script type="text/javascript">
$(document).ready(function() {
function refreshTreeDisclosureState() {
$('ol.sortable li').each(function() {
var $li = $(this);
var hasChildren = $li.children('ol').children('li').length > 0;
var $disclose = $li.children('div').children('.disclose');
if (hasChildren) {
$li.removeClass('sort-leaf');
if (!$li.hasClass('sort-collapsed') && !$li.hasClass('sort-expanded'))
$li.addClass('sort-collapsed');
$li.addClass('sort-branch');
$disclose.attr('aria-expanded', $li.hasClass('sort-expanded') ? 'true' : 'false');
$disclose.show();
} else {
$li.removeClass('sort-branch sort-collapsed sort-expanded').addClass('sort-leaf');
$disclose.attr('aria-expanded', 'false');
$disclose.hide();
}
});
}
function saveCategoriesOrder() {
var categories = $('#sortable').nestedSortable('toArray', { startDepthCount: 0 });
$.ajax({
type: 'POST',
cache: false,
url: '/admin/shop_category/save_categories_order/',
data: {
categories: categories
},
beforeSend: function() {
$.prompt('Trwa zapisywanie...', { title: 'Proszę czekać' });
},
success: function(data) {
$('.jqibox').remove();
var response = jQuery.parseJSON(data);
if (response.status === 'error')
create_error(response.msg);
}
});
}
$('body').on('click', '.category-delete', function() {
var category_id = $(this).attr('category-id');
$.prompt('Na pewno chcesz usunąć wybraną kategorię?', {
title: 'Potwierdź?',
buttons: { 'Tak': true, 'Nie': false },
submit: function(e, v) {
if (v === true)
document.location.href = '/admin/shop_category/category_delete/id=' + category_id;
}
});
});
$('ol.sortable').nestedSortable({
forcePlaceholderSize: true,
handle: 'div',
helper: 'clone',
items: 'li',
opacity: .9,
placeholder: 'placeholder',
revert: 250,
tabSize: 45,
tolerance: 'pointer',
toleranceElement: '> div',
maxLevels: 4,
isTree: true,
expandOnHover: 700,
protectRoot: false,
stop: function() {
saveCategoriesOrder();
}
});
refreshTreeDisclosureState();
$('body').on('click', '.disclose', function() {
var $li = $(this).closest('li');
if (!$li.hasClass('sort-branch')) return;
$li.toggleClass('sort-collapsed').toggleClass('sort-expanded');
$(this).attr('aria-expanded', $li.hasClass('sort-expanded') ? 'true' : 'false');
this.blur();
});
$('.disclose').mousedown(function(e) {
if (e.which === 1) {
var category_id = $(this).parent('div').parent('li').attr('id');
$.ajax({
type: 'POST',
cache: false,
url: '/admin/shop_category/cookie_categories/',
data: {
category_id: category_id
}
});
}
});
<?php
$array = isset($_COOKIE['cookie_categories']) ? @unserialize($_COOKIE['cookie_categories']) : [];
if (is_array($array)):
foreach ($array as $key => $val):
if ($val):
?>
$('#<?= $key; ?>').children('div').children('button.disclose').click();
<?php
endif;
endforeach;
endif;
?>
});
</script>

View File

@@ -0,0 +1,61 @@
<?
global $gdb;
ob_start();
?>
<div class="menu_sortable">
<ol class="sortable" id="sortable">
<? if ( is_array( $this -> categories ) ): foreach ( $this -> categories as $category ):?>
<li id="list_<?= $category['id'];?>" class="category_<?= $category['id'];?>" category="<?= $category['id'];?>">
<div class="context_0 content content_menu">
<button type="button" class="disclose layout-tree-toggle" aria-expanded="false" title="Rozwin / zwin">
<i class="fa fa-caret-right"></i>
</button>
<? if ( !$category['status'] ) echo '<i class="fa fa-ban fa-lg text-danger" title="Kategoria nieaktywna"></i>';?>
<b><?= $category['languages'][$this -> dlang]['title'];?></b>
<div class="btn-group ml20 pull-right">
<a href="/admin/shop_category/category_edit/id=<?= $category['id'];?>" title="Edytuj kategorię" class="btn btn-sm btn-system">
<i class="fa fa-file-text"></i> <span class="hidden-xs">Edytuj kategorię</span>
</a>
<a href="#" title="Usuń kategorię" class="btn btn-sm btn-danger category-delete" category-id="<?= $category['id'];?>">
<i class="fa fa-trash"></i> <span class="hidden-xs">Usuń kategorię</span>
</a>
<a href="/admin/shop_category/category_products/id=<?= $category['id'];?>" title="Lista produktów" class="btn btn-sm btn-info" category-id="<?= $category['id'];?>">
<i class="fa fa-bars"></i> <span class="hidden-xs">Lista produków</span>
</a>
<a href="/admin/shop_category/category_edit/pid=<?= $category['id'];?>" title="Dodaj podkategorię" class="btn btn-sm btn-success">
<i class="fa fa-plus-circle"></i> <span class="hidden-xs">Dodaj podkategorię</span>
</a>
</div>
</div>
<?= \Tpl::view( 'shop-category/subcategories-list', [
'categories' => ( new \Domain\Category\CategoryRepository( $GLOBALS['mdb'] ) )->subcategories( $category['id'] ),
'level' => $this -> level + 1,
'dlang' => $this -> dlang
] );?>
</li>
<? endforeach; endif;?>
</ol>
</div>
<?
$out = ob_get_clean();
$grid = new \gridEdit;
$grid -> id = 'pages-list';
$grid -> gdb_opt = $gdb;
$grid -> include_plugins = true;
$grid -> title = 'Lista kategorii';
$grid -> default_buttons = false;
$grid -> buttons = [
[
'label' => 'Dodaj kategorię',
'url' => '/admin/shop_category/category_edit/',
'icon' => 'fa-plus-circle',
'class' => 'btn-success'
]
];
$grid -> external_code = $out;
echo $grid -> draw();
?>
<?= \Tpl::view( 'shop-category/categories-list-custom-script' ); ?>

View File

@@ -0,0 +1,106 @@
<style type="text/css">
.layout-tree-toggle {
width: 18px;
height: 18px;
display: inline-flex;
align-items: center;
justify-content: center;
border: 0;
background: transparent;
padding: 0;
margin-right: 4px;
color: #666;
cursor: pointer;
}
.layout-tree-toggle:focus,
.layout-tree-toggle:active,
.layout-tree-toggle:focus-visible {
outline: none;
box-shadow: none;
}
li.sort-expanded > div .layout-tree-toggle i {
transform: rotate(90deg);
}
</style>
<script type="text/javascript" src="/libraries/jquery-nested-sortable/jquery.mjs.nestedSortable.js"></script>
<script type="text/javascript">
$(document).ready(function() {
function refreshTreeDisclosureState() {
$('ol.sortable li').each(function() {
var $li = $(this);
var hasChildren = $li.children('ol').children('li').length > 0;
var $disclose = $li.children('div').children('.disclose');
if (hasChildren) {
$li.removeClass('sort-leaf');
if (!$li.hasClass('sort-collapsed') && !$li.hasClass('sort-expanded'))
$li.addClass('sort-collapsed');
$li.addClass('sort-branch');
$disclose.attr('aria-expanded', $li.hasClass('sort-expanded') ? 'true' : 'false');
$disclose.show();
} else {
$li.removeClass('sort-branch sort-collapsed sort-expanded').addClass('sort-leaf');
$disclose.attr('aria-expanded', 'false');
$disclose.hide();
}
});
}
$('ol.sortable').nestedSortable({
forcePlaceholderSize: true,
handle: 'div',
helper: 'clone',
items: 'li',
opacity: .9,
placeholder: 'placeholder',
revert: 250,
tabSize: 45,
tolerance: 'pointer',
toleranceElement: '> div',
maxLevels: 4,
isTree: true,
expandOnHover: 700,
protectRoot: false
});
refreshTreeDisclosureState();
$('body').on('click', '.disclose', function() {
var $li = $(this).closest('li');
if (!$li.hasClass('sort-branch')) return;
$li.toggleClass('sort-collapsed').toggleClass('sort-expanded');
$(this).attr('aria-expanded', $li.hasClass('sort-expanded') ? 'true' : 'false');
this.blur();
});
$('.disclose').mousedown(function(e) {
if (e.which === 1) {
var category_id = $(this).parent('div').parent('li').attr('id');
$.ajax({
type: 'POST',
cache: false,
url: '/admin/shop_category/cookie_categories/',
data: {
category_id: category_id
}
});
}
});
<?php
$array = isset($_COOKIE['cookie_categories']) ? @unserialize($_COOKIE['cookie_categories']) : [];
if (is_array($array)):
foreach ($array as $key => $val):
if ($val):
?>
$('#<?= $key; ?>').children('div').children('button.disclose').click();
<?php
endif;
endforeach;
endif;
?>
});
</script>

View File

@@ -0,0 +1,59 @@
<?
global $gdb;
ob_start();
?>
<div class="menu_sortable">
<ol class="sortable" id="sortable">
<? if ( is_array( $this -> categories ) ): foreach ( $this -> categories as $category ):?>
<li id="list_<?= $category['id'];?>" class="category_<?= $category['id'];?>" category="<?= $category['id'];?>">
<div class="context_0 content content_menu">
<button type="button" class="disclose layout-tree-toggle" aria-expanded="false" title="Rozwin / zwin">
<i class="fa fa-caret-right"></i>
</button>
<? if ( !$category['status'] ) echo '<i class="fa fa-ban fa-lg text-danger" title="Kategoria nieaktywna"></i>';?>
<b><?= $category['languages'][$this -> dlang]['title'];?></b>
<div class="btn-group ml20 pull-right">
<a href="#" title="Wybierz kategorię" class="btn btn-sm btn-system button category-select" category-title="<?= $category['languages'][$this -> dlang]['title'];?>" category-id="<?= $category['id'];?>">
<i class="fa fa-check"></i> <span class="hidden-xs">wybierz</span>
</a>
</div>
</div>
<?= \Tpl::view( 'shop-category/subcategory-browse-list', [
'categories' => ( new \Domain\Category\CategoryRepository( $GLOBALS['mdb'] ) )->subcategories( $category['id'] ),
'level' => $this -> level + 1,
'dlang' => $this -> dlang
] );?>
</li>
<? endforeach; endif;?>
</ol>
</div>
<?
$out = ob_get_clean();
$grid = new \gridEdit;
$grid -> id = 'pages-list';
$grid -> gdb_opt = $gdb;
$grid -> include_plugins = false;
$grid -> title = 'Lista kategorii';
$grid -> default_buttons = false;
$grid -> external_code = $out;
echo $grid -> draw();
?>
<style type="text/css">
.mfp-container body.sb-top.sb-top-sm .navbar.navbar-fixed-top + #sidebar_left + #content_wrapper {
padding-top: 0;
}
.mfp-container #content {
padding: 27px 0 0;
}
.mfp-container #content > .row {
margin: 0;
}
.mfp-container #content > .row > .col-lg-9 {
padding: 0;
width: 100%;
float: none;
}
</style>
<?= \Tpl::view( 'shop-category/category-browse-custom-script' ); ?>

View File

@@ -0,0 +1,53 @@
<script type="text/javascript">
$( function() {
disable_menu();
$( '#settings-tabs' ).easyResponsiveTabs({
width: 'auto',
fit: true,
tabidentify: 'settings-tabs',
type: 'vertical'
});
$( '#languages-main' ).easyResponsiveTabs({
width: 'auto',
fit: true,
tabidentify: 'languages-main'
});
$( '#languages-seo' ).easyResponsiveTabs({
width: 'auto',
fit: true,
tabidentify: 'languages-seo'
});
});
function generate_seo_links( lang, title, category_id )
{
if ( title === '' )
return false;
$.ajax({
type: 'POST',
cache: false,
url: '/admin/pages/generateSeoLink/',
data: {
title: title,
category_id: category_id
},
beforeSend: function() {
$( '#overlay' ).show();
},
success: function( data ) {
$( '#overlay' ).hide();
var response = jQuery.parseJSON( data );
if ( response.status === 'ok' )
$( '#seo_link_' + lang ).val( response.seo_link );
else
create_error( response.msg );
}
});
}
</script>

View File

@@ -0,0 +1,209 @@
<script type="text/javascript" src="/libraries/ckeditor/ckeditor.js"></script>
<script type="text/javascript" src="/libraries/ckeditor/adapters/jquery.js"></script>
<?
global $db;
ob_start();
?>
<div id="settings-tabs">
<ul class="resp-tabs-list settings-tabs">
<li><i class="fa fa-file"></i>TreĹć</li>
<li><i class="fa fa-wrench"></i>Ustawienia</li>
<li><i class="fa fa-globe"></i>SEO</li>
</ul>
<div class="resp-tabs-container settings-tabs">
<div>
<div id="languages-main">
<ul class="resp-tabs-list languages-main htabs">
<? if ( is_array( $this -> languages ) ): foreach ( $this -> languages as $lg ):?>
<? if ( $lg['status'] ):?>
<li><? if ( $lg['id'] == \front\factory\Languages::default_language() ) echo '<i class="fa fa-star fa-lg text-system" title="JÄ™zyk domyĹlny"></i> ';?><?= $lg['name'];?></a></li>
<? endif;?>
<? endforeach; endif;?>
</ul>
<div class="resp-tabs-container languages-main">
<? if ( is_array( $this -> languages ) ): foreach ( $this -> languages as $lg ):?>
<? if ( $lg['status'] ):?>
<div>
<?= \Html::input(
array(
'label' => 'Nazwa kategorii',
'name' => 'title[' . $lg['id'] . ']',
'id' => 'title_' . $lg['id'],
'value' => $this -> category[ 'languages' ][ $lg['id'] ]['title'],
'inline' => true
)
);?>
<?= \Html::textarea(
array(
'label' => 'Opis kategorii',
'name' => 'text[' . $lg['id'] . ']',
'id' => 'text_' . $lg['id'],
'value' => $this -> category['languages'][ $lg['id'] ]['text'],
'inline' => true
)
);?>
<?= \Html::textarea(
array(
'label' => 'Opis kategorii (rozwinięcie)',
'name' => 'text_hidden[' . $lg['id'] . ']',
'id' => 'text_hidden_' . $lg['id'],
'value' => $this -> category['languages'][ $lg['id'] ]['text_hidden'],
'inline' => true
)
);?>
<?= \Html::textarea( [
'label' => 'Dodatkowy tekst (nad produktami)',
'name' => 'additional_text[' . $lg['id'] . ']',
'id' => 'additional_text_' . $lg['id'],
'value' => $this -> category['languages'][ $lg['id'] ]['additional_text'],
'inline' => true
] );?>
<script type="text/javascript">
$( function() {
$( '#text_<?= $lg['id'];?>, #text_hidden_<?= $lg['id'];?>, #additional_text_<?= $lg['id'];?>' ).ckeditor( {
toolbar : 'MyToolbar',
height:'250'
});
});
</script>
</div>
<? endif;?>
<? endforeach; endif;?>
</div>
<div class="clear"></div>
</div>
</div>
<div style="padding: 15px;">
<?= \Html::input_switch(
array(
'label' => 'Aktywna',
'name' => 'status',
'checked' => $this -> category['status'] == 1 or !$this -> category['id'] ? true : false
)
);?>
<?= \Html::select(
[
'label' => 'Sortowanie produktĂłw',
'name' => 'sort_type',
'id' => 'sort_type',
'values' => is_array( $this -> sort_types ) ? $this -> sort_types : [],
'value' => $this -> category['sort_type']
]
);?>
<?= \Html::input_switch(
array(
'label' => 'WyĹwietlić podkategorie',
'name' => 'view_subcategories',
'checked' => $this -> category['view_subcategories'] == 1 ? true : false
)
);?>
</div>
<div>
<div id="languages-seo">
<ul class="resp-tabs-list languages-seo htabs">
<? if ( is_array( $this -> languages ) ): foreach ( $this -> languages as $lg ):?>
<? if ( $lg['status'] ):?>
<li><? if ( $lg['id'] == \front\factory\Languages::default_language() ) echo '<i class="fa fa-star fa-lg text-system" title="JÄ™zyk domyĹlny"></i> ';?><?= $lg['name'];?></a></li>
<? endif;?>
<? endforeach; endif;?>
</ul>
<div class="resp-tabs-container languages-seo">
<? if ( is_array( $this -> languages ) ): foreach ( $this -> languages as $lg ):?>
<? if ( $lg['status'] ):?>
<div>
<?= \Html::input_icon(
array(
'label' => 'Link SEO',
'name' => 'seo_link[' . $lg['id'] . ']',
'id' => 'seo_link_' . $lg['id'],
'value' => $this -> category['languages' ][ $lg['id'] ]['seo_link'],
'icon_content' => 'generuj',
'icon_js' => 'generate_seo_links( "' . $lg['id'] . '", $( "#title_' . $lg['id'] . '" ).val(), ' . (int)$this -> category['id'] . ' );'
)
);?>
<?= \Html::input(
array(
'label' => 'TytuĹ kategorii (h1)',
'name' => 'category_title[' . $lg['id'] . ']',
'id' => 'category_title_' . $lg['id'],
'value' => $this -> category['languages' ][ $lg['id'] ]['category_title']
)
);?>
<?= \Html::input(
array(
'label' => 'Meta title',
'name' => 'meta_title[' . $lg['id'] . ']',
'id' => 'meta_title_' . $lg['id'],
'value' => $this -> category['languages'][ $lg['id'] ]['meta_title']
)
);?>
<?= \Html::textarea(
array(
'label' => 'Meta description',
'name' => 'meta_description[' . $lg['id'] . ']',
'id' => 'meta_description_' . $lg['id'],
'value' => $this -> category['languages'][ $lg['id'] ]['meta_description']
)
);?>
<?= \Html::textarea(
array(
'label' => 'Meta keywords',
'name' => 'meta_keywords[' . $lg['id'] . ']',
'id' => 'meta_keywords_' . $lg['id'],
'value' => $this -> category['languages'][ $lg['id'] ]['meta_keywords']
)
);?>
<?= \Html::select(
array(
'label' => 'Blokuj indeksacjÄ™',
'name' => 'noindex[' . $lg['id'] . ']',
'id' => 'noindex_' . $lg['id'],
'values' => array(
0 => 'nie', 1 => 'tak'
),
'value' => $this -> category['languages'][ $lg['id'] ]['noindex'] == 1 ? 1 : 0
)
);?>
</div>
<? endif;?>
<? endforeach; endif;?>
</div>
<div class="clear"></div>
</div>
</div>
</div>
<div class="clear"></div>
<?
$out = ob_get_clean();
$grid = new \gridEdit;
$grid -> id = 'category-edit';
$grid -> gdb_opt = $gdb;
$grid -> include_plugins = true;
$grid -> title = 'Edycja kategorii';
$grid -> fields = [
[
'db' => 'id',
'type' => 'hidden',
'value' => $this -> category['id']
],
[
'db' => 'parent_id',
'type' => 'hidden',
'value' => $this -> category['id'] ? $this -> category['parent_id'] : $this -> pid
]
];
$grid -> actions = [
'save' => [ 'url' => '/admin/shop_category/save/', 'back_url' => '/admin/shop_category/view_list/' ],
'cancel' => [ 'url' => '/admin/shop_category/view_list/' ]
];
$grid -> external_code = $out;
$grid -> persist_edit = true;
$grid -> id_param = 'id';
echo $grid -> draw();
?>
<?= \Tpl::view( 'shop-category/category-edit-custom-script' ); ?>

View File

@@ -0,0 +1,62 @@
<style type="text/css">
.layout-tree-toggle {
width: 18px;
height: 18px;
display: inline-flex;
align-items: center;
justify-content: center;
border: 0;
background: transparent;
padding: 0;
margin-right: 6px;
color: #666;
cursor: move;
}
</style>
<script type="text/javascript" src="/libraries/jquery-nested-sortable/jquery.mjs.nestedSortable.js"></script>
<script type="text/javascript">
$(document).ready(function() {
function saveProductsOrder() {
var products = $('ol.sortable').nestedSortable('toArray', { startDepthCount: 0 });
$.ajax({
type: 'POST',
cache: false,
url: '/admin/shop_category/save_products_order/',
data: {
category_id: <?= (int)$this->category_id; ?>,
products: products
},
beforeSend: function() {
$.prompt('Trwa zapisywanie...', { title: 'Proszę czekać' });
},
success: function(data) {
$('.jqibox').remove();
var response = jQuery.parseJSON(data);
if (response.status === 'error')
create_error(response.msg);
}
});
}
$('ol.sortable').nestedSortable({
forcePlaceholderSize: true,
handle: 'div',
helper: 'clone',
items: 'li',
opacity: .6,
placeholder: 'placeholder',
revert: 250,
tabSize: 25,
tolerance: 'pointer',
toleranceElement: '> div',
maxLevels: 1,
isTree: true,
expandOnHover: 700,
stop: function() {
saveProductsOrder();
}
});
});
</script>

View File

@@ -0,0 +1,37 @@
<?
global $gdb;
ob_start();
?>
<ol class="sortable" id="article-list">
<?
if ( is_array( $this -> products ) ) foreach ( $this -> products as $product )
{
?>
<li id="list_<?= $product['product_id'];?>">
<div class="content <? if ( !$product['status'] ) echo 'text-danger';?>"><button type="button" class="disclose layout-tree-toggle" aria-expanded="false" title="Przesuń"><i class="fa fa-arrows"></i></button> <?= $product['name'];?></div>
</li>
<?
}
?>
</ol>
<?
$out = ob_get_clean();
$grid = new \gridEdit;
$grid -> gdb_opt = $gdb;
$grid -> include_plugins = true;
$grid -> default_buttons = false;
$grid -> external_code = $out;
$grid -> title = 'Lista produktów';
$grid -> buttons = [
[
'label' => 'Wstecz',
'url' => '/admin/shop_category/view_list/',
'icon' => 'fa-reply',
'class' => 'btn-dark'
]
];
echo $grid -> draw();
?>
<?= \Tpl::view( 'shop-category/category-products-custom-script', [ 'category_id' => $this -> category_id ] ); ?>

View File

@@ -0,0 +1,36 @@
<? if ( is_array( $this -> categories ) ):?>
<ol>
<? foreach ( $this -> categories as $category ):?>
<li id="list_<?= $category['id'];?>" class="list_<?= $category['id'];?>" category="<?= $category['id'];?>">
<div class="context_0 content content_menu">
<button type="button" class="disclose layout-tree-toggle" aria-expanded="false" title="Rozwin / zwin">
<i class="fa fa-caret-right"></i>
</button>
<? if ( !$category['status'] ) echo '<i class="fa fa-ban fa-lg text-danger" title="Kategoria nieaktywna"></i>';?>
<b><?= $category['languages'][$this -> dlang]['title'];?></b>
<div class="btn-group ml20 pull-right">
<a href="/admin/shop_category/category_edit/id=<?= $category['id'];?>" title="Edytuj kategorię" class="btn btn-sm btn-system">
<i class="fa fa-file-text"></i> <span class="hidden-xs">Edytuj kategorię</span>
</a>
<a href="#" title="Usuń kategorię" class="btn btn-sm btn-danger category-delete" category-id="<?= $category['id'];?>">
<i class="fa fa-trash"></i> <span class="hidden-xs">Usuń kategorię</span>
</a>
<a href="/admin/shop_category/category_products/id=<?= $category['id'];?>" title="Lista produktów" class="btn btn-sm btn-info" category-id="<?= $category['id'];?>">
<i class="fa fa-bars"></i> <span class="hidden-xs">Lista produków</span>
</a>
<? if ( $this -> level < 2 ):?>
<a href="/admin/shop_category/category_edit/pid=<?= $category['id'];?>" title="Dodaj podkategorię" class="btn btn-sm btn-success">
<i class="fa fa-plus-circle"></i> <span class="hidden-xs">Dodaj podkategorię</span>
</a>
<? endif;?>
</div>
</div>
<?= \Tpl::view( 'shop-category/subcategories-list', [
'categories' => ( new \Domain\Category\CategoryRepository( $GLOBALS['mdb'] ) )->subcategories( $category['id'] ),
'level' => $this -> level + 1,
'dlang' => $this -> dlang
] );?>
</li>
<? endforeach;?>
</ol>
<? endif;?>

View File

@@ -0,0 +1,25 @@
<? if ( is_array( $this -> categories ) ):?>
<ol>
<? foreach ( $this -> categories as $category ):?>
<li id="list_<?= $category['id'];?>" class="list_<?= $category['id'];?>" category="<?= $category['id'];?>">
<div class="context_0 content content_menu">
<button type="button" class="disclose layout-tree-toggle" aria-expanded="false" title="Rozwin / zwin">
<i class="fa fa-caret-right"></i>
</button>
<? if ( !$category['status'] ) echo '<i class="fa fa-ban fa-lg text-danger" title="Kategoria nieaktywna"></i>';?>
<b><?= $category['languages'][$this -> dlang]['title'];?></b>
<div class="btn-group ml20 pull-right">
<a href="#" title="Wybierz kategorię" class="btn btn-sm btn-system button category-select" category-title="<?= $category['languages'][$this -> dlang]['title'];?>" category-id="<?= $category['id'];?>">
<i class="fa fa-check"></i> <span class="hidden-xs">wybierz</span>
</a>
</div>
</div>
<?= \Tpl::view( 'shop-category/subcategory-browse-list', [
'categories' => ( new \Domain\Category\CategoryRepository( $GLOBALS['mdb'] ) )->subcategories( $category['id'] ),
'level' => $this -> level + 1,
'dlang' => $this -> dlang
] );?>
</li>
<? endforeach;?>
</ol>
<? endif;?>

View File

@@ -0,0 +1,114 @@
<style type="text/css">
.layout-tree-toggle {
width: 18px;
height: 18px;
display: inline-flex;
align-items: center;
justify-content: center;
border: 0;
background: transparent;
padding: 0;
margin-right: 4px;
color: #666;
cursor: pointer;
}
.layout-tree-toggle:focus,
.layout-tree-toggle:active,
.layout-tree-toggle:focus-visible {
outline: none;
box-shadow: none;
}
li.sort-expanded > div .layout-tree-toggle i {
transform: rotate(90deg);
}
#mass-edit-panel .product-item {
margin-bottom: 4px;
display: flex;
align-items: center;
gap: 6px;
}
#mass-edit-panel .product-item label {
cursor: pointer;
font-weight: normal;
margin-bottom: 0;
}
#mass-edit-panel .content_menu .icheckbox_minimal-blue {
margin-top: 0;
}
#mass-edit-panel .icheckbox_minimal-blue {
position: relative;
overflow: hidden;
}
</style>
<link rel="stylesheet" type="text/css" href="/libraries/grid/plugins/icheck/skins/minimal/minimal.css">
<link rel="stylesheet" type="text/css" href="/libraries/grid/plugins/icheck/skins/minimal/blue.css">
<div class="panel mb50 panel-primary" id="mass-edit-panel">
<div class="panel-heading">
<span class="panel-title">Masowa edycja produktów</span>
</div>
<div class="panel-body">
<button class="btn btn-dark select-all">Zaznacz wszystkie</button>
<button class="btn btn-default btn-outline-dark deselect-all">Odznacz wszystkie</button>
</div>
<div class="panel-body">
<div class="form-group mb10">
<label class="col-lg-3 control-label" for="discount_percent">Ustaw cenę promocyjną (minus X procent)</label>
<div class="col-lg-9">
<div class="bs-component">
<div class="input-group">
<input class="form-control" type="text" id="discount_percent" name="discount_percent">
<span class="input-group-addon cursor" field-id="discount_percent">
<i class="fa fa-save"></i>
</span>
</div>
</div>
</div>
</div>
</div>
<div class="panel-body">
<div class="row">
<div class="col-lg-6">
<?php if ( is_array( $this->products ) ): foreach ( $this->products as $key => $product ): ?>
<div class="product-item">
<input type="checkbox" class="g-checkbox" name="products[]" id="product<?= $key; ?>" value="<?= $key; ?>">
<label for="product<?= $key; ?>"><?= htmlspecialchars( $product ); ?></label>
</div>
<?php endforeach; endif; ?>
</div>
<div class="col-lg-6">
<div class="menu_sortable">
<ol class="sortable" id="sortable">
<?php if ( is_array( $this->categories ) ): foreach ( $this->categories as $category ): ?>
<li id="list_<?= $category['id']; ?>" class="category_<?= $category['id']; ?>" category="<?= $category['id']; ?>">
<div class="context_0 content content_menu">
<button type="button" class="disclose layout-tree-toggle" aria-expanded="false" title="Rozwin / zwin">
<i class="fa fa-caret-right"></i>
</button>
<?php if ( !$category['status'] ) echo '<i class="fa fa-ban fa-lg text-danger" title="Kategoria nieaktywna"></i>'; ?>
<input type="checkbox" class="g-checkbox" name="mass_categories[]" value="<?= $category['id']; ?>" />
<b><?= $category['languages'][$this->dlang]['title']; ?></b>
</div>
<?= \Tpl::view( 'shop-product/subcategories-list', [
'categories' => ( new \Domain\Category\CategoryRepository( $GLOBALS['mdb'] ) )->subcategories( $category['id'] ),
'level' => ($this->level ?? 0) + 1,
'dlang' => $this->dlang,
'name' => 'mass_categories[]'
] ); ?>
</li>
<?php endforeach; endif; ?>
</ol>
</div>
</div>
</div>
</div>
</div>
<?= \Tpl::view( 'shop-product/mass-edit-custom-script' ); ?>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,24 @@
<? if ( is_array( $this -> categories ) ):?>
<ol>
<? foreach ( $this -> categories as $category ):?>
<li id="list_<?= $category[ 'id' ];?>" class="list_<?= $category[ 'id' ];?>" category="<?= $category[ 'id' ];?>">
<div class="context_0 content content_menu">
<button type="button" class="disclose layout-tree-toggle" aria-expanded="false" title="Rozwin / zwin">
<i class="fa fa-caret-right"></i>
</button>
<? if ( !$category[ 'status' ] ) echo '<i class="fa fa-ban fa-lg text-danger" title="Kategoria nieaktywna"></i>';?>
<input type="checkbox" class="g-checkbox" name="<?= $this -> name ? $this -> name : 'categories[]';?>" value="<?= $category[ 'id' ];?>" <? if ( is_array( $this -> product_categories ) and in_array( $category[ 'id' ], $this -> product_categories ) ):?>checked="checked"<? endif;?> />
<b><?= $category[ 'languages' ][ $this -> dlang ][ 'title' ];?></b>
</div>
<?=
\Tpl::view( 'shop-product/subcategories-list', [
'categories' => ( new \Domain\Category\CategoryRepository( $GLOBALS['mdb'] ) )->subcategories( $category[ 'id' ] ),
'product_categories' => $this -> product_categories,
'dlang' => $this -> dlang,
'name' => $this -> name
] );
?>
</li>
<? endforeach;?>
</ol>
<? endif;?>

View File

@@ -0,0 +1,273 @@
<? global $user, $settings;?>
<!DOCTYPE html>
<html>
<head>
<title>shopPro</title>
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta http-equiv="content-type" content="text/html; charset=utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="author" content="www.project-pro.pl - internetowe rozwi&#261;zania dla biznesu">
<link rel='stylesheet' type="text/css" href='https://fonts.googleapis.com/css?family=Open+Sans:300,400,600,700'>
<link rel="stylesheet" type="text/css" href="/libraries/framework/skin/default_skin/css/theme.css">
<link rel="stylesheet" type="text/css" href="/libraries/framework/vendor/plugins/magnific/magnific-popup.css">
<link rel="stylesheet" type="text/css" href="/libraries/framework/vendor/plugins/datepicker/css/bootstrap-datetimepicker.css">
<link rel="stylesheet" type="text/css" href="/libraries/framework/vendor/jquery/jquery_ui/jquery-ui.structure.min.css">
<link rel="stylesheet" type="text/css" href="/libraries/framework/vendor/jquery/jquery_ui/jquery-ui.theme.min.css">
<link rel="stylesheet" type="text/css" href="/libraries/framework/vendor/plugins/daterange/daterangepicker.css">
<link rel="stylesheet" type="text/css" href="/libraries/jquery-confirm/jquery-confirm.min.css">
<link rel="stylesheet" type="text/css" href="/libraries/easy-tabs/css/easy-responsive-tabs.css">
<link rel="stylesheet" type="text/css" href="/libraries/bootstrap-4.5.2-dist/css/bootstrap.css">
<link rel="stylesheet" type="text/css" href="/libraries/font-awesome-4.7.0/css/font-awesome.css">
<link rel="stylesheet" type="text/css" href="/libraries/grid/plugins/icheck/skins/square/blue.css">
<script type="text/javascript" src="/libraries/framework/vendor/jquery/jquery-1.11.1.min.js"></script>
<script type="text/javascript" src="/libraries/framework/vendor/jquery/jquery_ui/jquery-ui.min.js"></script>
<script type="text/javascript" src="/libraries/framework/js/utility/utility.js"></script>
<script type="text/javascript" src="/libraries/framework/vendor/plugins/magnific/jquery.magnific-popup.js"></script>
<script type="text/javascript" src="/libraries/easy-tabs/js/easyResponsiveTabs.js"></script>
<script type="text/javascript" src="/libraries/framework/vendor/plugins/moment/moment.js"></script>
<script type="text/javascript" src="/libraries/framework/vendor/plugins/moment/pl.js"></script>
<script type="text/javascript" src="/libraries/framework/vendor/plugins/datepicker/js/bootstrap-datetimepicker.js"></script>
<script type="text/javascript" src="/libraries/framework/vendor/plugins/daterange/daterangepicker.js"></script>
<script type="text/javascript" src="/libraries/jquery-confirm/jquery-confirm.min.js"></script>
<script type="text/javascript" src="/libraries/bootstrap-4.5.2-dist/js/bootstrap.min.js"></script>
<script type="text/javascript" src="/libraries/bootstrap-4.5.2-dist/js/bootstrap.bundle.min.js"></script>
<script type="text/javascript" src="/libraries/grid/plugins/icheck/icheck.js"></script>
<script type="text/javascript" src="/libraries/functions.js"></script>
<script type="text/javascript" src="/admin/js/functions.js"></script>
<link rel="stylesheet" href="/admin/layout/style-css/style.css" />
</head>
<body>
<div class="admin-page">
<div class="menu">
<div class="logo sticky-top">
shop<b>Pro</b>
<span>ver. <?= \S::get_version();?></span><br>
<? if ( $settings[ 'update' ] and \S::get_new_version() > \S::get_version() ):?>
<a href="/admin/update/main_view/" class="label label-danger">aktualizacja</a>
<? endif;?>
</div>
<div class="menu-content">
<ul>
<li><a href="/admin/dashboard/main_view/"><img src="/admin/layout/icon/icon-menu/bxs-dashboard.svg">Pulpit</a></li>
</ul>
<div class="title">
Sklep
</div>
<ul>
<li> <a href="/admin/shop_order/view_list/"><img src="/admin/layout/icon/icon-menu/shopping-cart.svg">Zam&#243;wienia</a></li>
<li> <a href="/admin/shop_clients/list/"><img src="/admin/layout/icon/icon-menu/people-fill.svg">Klienci</a></li>
<li><a href="/admin/shop_category/list/"><img src="/admin/layout/icon/icon-menu/bxs-category-alt.svg">Kategorie</a></li>
<li><a href="/admin/shop_product/view_list/"><img src="/admin/layout/icon/icon-menu/shopping-basket.svg">Produkty</a></li>
<li><a href="/admin/shop_product/mass_edit/"><i class="fa fa-bars"></i>Masowa edycja</a></li>
<li>
<a href="/admin/shop_producer/list/">
<i class="fa fa-user"></i>Producenci
</a>
</li>
<li>
<a href="/admin/shop_product_sets/list/">
<i class="fa fa-bars"></i>Komplety Produkt&#243;w
</a>
</li>
<li><a href="/admin/shop_attribute/list/"><img src="/admin/layout/icon/icon-menu/star-filled.svg">Cechy produkt&#243;w</a></li>
<li><a href="/admin/shop_transport/list/"><img src="/admin/layout/icon/icon-menu/bus.svg">Rodzaje transportu</a></li>
<li><a href="/admin/shop_payment_method/list/"><img src="/admin/layout/icon/icon-menu/coins-fill.svg">Metody p&#322;atno&#347;ci</a></li>
<li>
<a href="/admin/shop_statuses/list/"><i class="fa fa-bars"></i>Statusy zam&#243;wie&#324;</a>
</li>
<li><a href="/admin/shop_coupon/list/"><img src="/admin/layout/icon/icon-menu/piggy-bank-coins.svg">Kody rabatowe</a></li>
<li><a href="/admin/shop_promotion/list/"><img src="/admin/layout/icon/icon-menu/burst-sale.svg">Promocje</a></li>
</ul>
<div class="title">Zawarto&#347;&#263;</div>
<ul>
<li><a href="/admin/pages/list/"><img src="/admin/layout/icon/icon-menu/menu(1).svg">Konfiguracja menu</a></li>
<li><a href="/admin/articles/list/"><img src="/admin/layout/icon/icon-menu/note.svg">Artyku&#322;y</a></li>
<li><a href="/admin/articles_archive/list/"><img src="/admin/layout/icon/icon-menu/baseline-sticky-note-2.svg">Archiwum artyku&#322;&#243;w</a></li>
<li><a href="/admin/scontainers/list/"><img src="/admin/layout/icon/icon-menu/view-cols.svg">Zawarto&#347;&#263; statyczna</a></li>
</ul>
<div class="title">
Newsletter
</div>
<ul>
<li><a href="/admin/newsletter/emails_list/"><img src="/admin/layout/icon/icon-menu/bx-at.svg">Baza emaili</a></li>
<li><a href="/admin/newsletter/email_templates_admin/"><img src="/admin/layout/icon/icon-menu/template.svg">Szablony administracyjne</a></li>
<li><a href="/admin/newsletter/settings/"><img src="/admin/layout/icon/icon-menu/baseline-settings.svg">Ustawienia</a></li>
</ul>
<div class="title">
Pozosta&#322;e
</div>
<ul>
<li><a href="/admin/filemanager/draw/"><img src="/admin/layout/icon/icon-menu/file-image-outlined.svg">Menad&#380;er plik&#243;w</a></li>
<li><a href="/admin/banners/list/"><img src="/admin/layout/icon/icon-menu/photo-video.svg">Banery</a></li>
</ul>
<div class="title">
Konfiguracja
</div>
<ul>
<li><a href="/admin/users/view_list/"><img src="/admin/layout/icon/icon-menu/people-fill.svg">U&#380;ytkownicy</a></li>
<li><a href="/admin/languages/view_list/"><img src="/admin/layout/icon/icon-menu/letter-aa.svg">J&#281;zyki</a></li>
<li><a href="/admin/languages/translation_list/"><img src="/admin/layout/icon/icon-menu/language.svg">T&#322;umaczenia</a></li>
<li><a href="/admin/settings/view/"><img src="/admin/layout/icon/icon-menu/baseline-settings.svg">Ustawienia</a></li>
<li><a href="/admin/layouts/list/"><img src="/admin/layout/icon/icon-menu/calendar-settings.svg">Szablony</a></li>
</ul>
<div class="title">
S&#322;owniki
</div>
<ul>
<li>
<a href="/admin/dictionaries/list/">
<i class="fa fa-puzzle-piece" aria-hidden="true"></i>Jednostki miary
</a>
</li>
</ul>
<div class="title">
Archiwum
</div>
<ul>
<li>
<a href="/admin/product_archive/list/">
<i class="fa fa-trash" aria-hidden="true"></i>Produkty
</a>
</li>
</ul>
<div class="title">
Integracje
</div>
<ul>
<li>
<a href="/admin/integrations/apilo_settings/">
<i class="fa fa-cogs" aria-hidden="true"></i>apilo.com
</a>
</li>
<li>
<a href="/admin/integrations/shoppro_settings/">
<i class="fa fa-cogs" aria-hidden="true"></i>shopPRO
</a>
</li>
</ul>
</div>
<div class="preview">
<a href="/" target="_blank"><img src="/admin/layout/icon/icon-menu/chevrons-right.svg">Podgl&#261;d</a>
</div>
</div>
<div class="mobile-menu-btn" id="mobile-menu-btn">
<i class="fa fa-bars"></i>
</div>
<div class="site-content">
<div class="container-fluid">
<div class="row">
<div class="col-12 col-md-3 col-lg-2">
<button id="clear-cache-btn" class="btn btn-danger mt-3">Wyczy&#347;&#263; cache</button>
</div>
<div class="col-12 col-md-9 col-lg-10 top-user">
<div class="dropdown">
<?
if ( $user[ 'name' ] or $user[ 'surname' ] )
echo $user[ 'surname' ] . ' ' . $user[ 'name' ];
else
echo $user[ 'login' ];
?>
<img src="/admin/layout/icon/user-avatar-filled.svg" alt="avatar" >
<div class="dropdown-content">
<a href="/admin/?s-action=user-logout"><span class="fa fa-power-off"></span>Wyloguj si&#281;</a>
</div>
</div>
</div>
</div>
<?
if ( $alert = \S::get_session( 'alert' ) ):
\S::alert( false );
?>
<div class="row">
<div class=" col-12">
<div class="alert alert-danger alert-dismissable">
<button type="button" class="close" data-dismiss="alert" aria-hidden="true">&times;</button>
<i class="fa fa-info pr10"></i>
<?= $alert;?>
</div>
</div>
</div>
<? endif;?>
<div class="row">
<div class="col-12 dashboard-page" id="content">
<?= $this -> content;?>
</div>
</div>
</div>
</div>
</div>
<script class="footer" type="text/javascript">
(function() {
function bindClearCacheButton() {
$(document)
.off('click.clearCacheBtn', '#clear-cache-btn')
.on('click.clearCacheBtn', '#clear-cache-btn', function(e) {
e.preventDefault();
var $btn = $(this);
var originalText = $btn.text();
// Show pending state
$btn.prop('disabled', true).html('<i class="fa fa-spinner fa-spin"></i> Czyszcz\u0119 cache...');
// Send AJAX request
$.ajax({
url: '/admin/settings/clearCacheAjax/',
type: 'POST',
dataType: 'json',
success: function(response) {
if (response.status === 'success') {
// Show success state
$btn.html('<i class="fa fa-check"></i> Cache wyczyszczony!').removeClass('btn-danger').addClass('btn-success');
// Restore original state after delay
setTimeout(function() {
$btn.prop('disabled', false).html(originalText).removeClass('btn-success').addClass('btn-danger');
}, 2000);
} else {
// Request failed on server
$btn.html('<i class="fa fa-exclamation-triangle"></i> B\u0142\u0105d!').removeClass('btn-danger').addClass('btn-warning');
setTimeout(function() {
$btn.prop('disabled', false).html(originalText).removeClass('btn-warning').addClass('btn-danger');
}, 2000);
}
},
error: function() {
// Network or transport error
$btn.html('<i class="fa fa-exclamation-triangle"></i> B\u0142\u0105d po\u0142\u0105czenia!').removeClass('btn-danger').addClass('btn-warning');
setTimeout(function() {
$btn.prop('disabled', false).html(originalText).removeClass('btn-warning').addClass('btn-danger');
}, 2000);
}
});
});
}
bindClearCacheButton();
})();
$(document).ready(function () {
var user_agent = navigator.userAgent.toLowerCase();
var click_event = user_agent.match(/(iphone|ipod|ipad)/) ? "touchend" : "click";
$('body').on(click_event, '#mobile-menu-btn', function ()
{
if ($(this).hasClass('active'))
{
$(this).removeClass('active');
$( '.menu' ).removeClass( 'visible' );
$( '#mobile-menu-btn i' ).addClass( 'fa-bars' ).removeClass( 'fa-times' );
$( '.site-content' ).removeClass( 'with-menu' );
}
else
{
$(this).addClass('active');
$( '.menu' ).addClass( 'visible' );
$( '.site-content' ).addClass( 'with-menu' );
$( '#mobile-menu-btn i' ).addClass( 'fa-times' ).removeClass( 'fa-bars' );
}
});
});
</script>
</body>
</html>

View File

@@ -0,0 +1,163 @@
<?php
namespace admin\Controllers;
use Domain\Category\CategoryRepository;
use Domain\Languages\LanguagesRepository;
class ShopCategoryController
{
private CategoryRepository $repository;
private LanguagesRepository $languagesRepository;
public function __construct(CategoryRepository $repository, LanguagesRepository $languagesRepository)
{
$this->repository = $repository;
$this->languagesRepository = $languagesRepository;
}
public function view_list(): string
{
return \Tpl::view('shop-category/categories-list', [
'categories' => $this->repository->subcategories(null),
'level' => 0,
'dlang' => \front\factory\Languages::default_language(),
]);
}
public function list(): string
{
return $this->view_list();
}
public function category_edit(): string
{
return \Tpl::view('shop-category/category-edit', [
'category' => $this->repository->categoryDetails(\S::get('id')),
'pid' => \S::get('pid'),
'languages' => $this->languagesRepository->languagesList(),
'sort_types' => $this->repository->sortTypes(),
]);
}
public function edit(): string
{
return $this->category_edit();
}
public function save(): void
{
$response = [
'status' => 'error',
'msg' => 'Podczas zapisywania kategorii wystąpił błąd. Proszę spróbować ponownie.',
];
$values = json_decode((string)\S::get('values'), true);
if (is_array($values)) {
$savedId = $this->repository->save($values);
if (!empty($savedId)) {
$response = [
'status' => 'ok',
'msg' => 'Kategoria została zapisana.',
'id' => (int)$savedId,
];
}
}
echo json_encode($response);
exit;
}
public function category_delete(): void
{
if ($this->repository->categoryDelete(\S::get('id'))) {
\S::set_message('Kategoria została usunięta.');
} else {
\S::alert('Podczas usuwania kategorii wystąpił błąd. Aby usunąć kategorię nie może ona posiadać przypiętych podkategorii.');
}
header('Location: /admin/shop_category/view_list/');
exit;
}
public function delete(): void
{
$this->category_delete();
}
public function category_products(): string
{
return \Tpl::view('shop-category/category-products', [
'category_id' => \S::get('id'),
'products' => $this->repository->categoryProducts((int)\S::get('id')),
]);
}
public function products(): string
{
return $this->category_products();
}
public function category_url_browser(): void
{
echo \Tpl::view('shop-category/category-browse-list', [
'categories' => $this->repository->subcategories(null),
'level' => 0,
'dlang' => \front\factory\Languages::default_language(),
]);
exit;
}
public function save_categories_order(): void
{
$response = [
'status' => 'error',
'msg' => 'Podczas zapisywania kolejności kategorii wystąpił błąd. Proszę spróbować ponownie.',
];
if ( $this->repository->saveCategoriesOrder( \S::get( 'categories' ) ) ) {
$response = [ 'status' => 'ok' ];
}
echo json_encode( $response );
exit;
}
public function save_products_order(): void
{
$response = [
'status' => 'error',
'msg' => 'Podczas zapisywania kolejności wyświetlania produktów wystąpił błąd. Proszę spróbować ponownie.',
];
if ( $this->repository->saveProductOrder( \S::get( 'category_id' ), \S::get( 'products' ) ) ) {
$response = [ 'status' => 'ok' ];
}
echo json_encode( $response );
exit;
}
public function cookie_categories(): void
{
$categoryId = (string) \S::get( 'category_id' );
if ( $categoryId === '' ) {
echo json_encode( [ 'status' => 'error' ] );
exit;
}
$array = [];
if ( isset( $_COOKIE['cookie_categories'] ) ) {
$tmp = @unserialize( (string) $_COOKIE['cookie_categories'] );
if ( is_array( $tmp ) ) {
$array = $tmp;
}
}
$array[$categoryId] = isset( $array[$categoryId] ) && (int) $array[$categoryId] === 1 ? 0 : 1;
setcookie( 'cookie_categories', serialize( $array ), time() + 3600 * 24 * 365, '/' );
echo json_encode( [ 'status' => 'ok' ] );
exit;
}
}

View File

@@ -0,0 +1,73 @@
<?php
namespace admin\Controllers;
use Domain\Product\ProductRepository;
use Domain\Category\CategoryRepository;
/**
* Kontroler masowej edycji produktów.
* Obsługuje akcje: mass_edit (widok), mass_edit_save (AJAX), get_products_by_category (AJAX).
* Pozostałe akcje shop_product (view_list, product_edit, save itd.) nadal działają
* przez fallback na \admin\controls\ShopProduct.
*/
class ShopProductController
{
private ProductRepository $repository;
public function __construct(ProductRepository $repository)
{
$this->repository = $repository;
}
/**
* Widok masowej edycji produktów.
*/
public function mass_edit(): string
{
$categoryRepository = new CategoryRepository( $GLOBALS['mdb'] );
return \Tpl::view( 'shop-product/mass-edit', [
'products' => $this->repository->allProductsForMassEdit(),
'categories' => $categoryRepository->subcategories( null ),
'dlang' => \front\factory\Languages::default_language()
] );
}
/**
* AJAX: zastosowanie rabatu procentowego na zaznaczonych produktach.
*/
public function mass_edit_save(): void
{
$discountPercent = \S::get( 'discount_percent' );
$products = \S::get( 'products' );
if ( $discountPercent != '' && $products && is_array( $products ) && count( $products ) > 0 ) {
$productId = (int) $products[0];
$result = $this->repository->applyDiscountPercent( $productId, (float) $discountPercent );
if ( $result !== null ) {
echo json_encode( [
'status' => 'ok',
'price_brutto_promo' => $result['price_brutto_promo'],
'price_brutto' => $result['price_brutto']
] );
exit;
}
}
echo json_encode( [ 'status' => 'error' ] );
exit;
}
/**
* AJAX: pobranie ID produktów z danej kategorii.
*/
public function get_products_by_category(): void
{
$categoryId = (int) \S::get( 'category_id' );
$products = $this->repository->getProductsByCategory( $categoryId );
echo json_encode( [ 'status' => 'ok', 'products' => $products ] );
exit;
}
}

View File

@@ -0,0 +1,493 @@
<?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()
{
global $mdb;
$sa = \S::get('s-action');
$domain = preg_replace('/^www\./', '', $_SERVER['SERVER_NAME']);
$cookie_name = 'admin_remember_' . str_replace( '.', '-', $domain );
$users = new \Domain\User\UserRepository($mdb);
switch ($sa)
{
case 'user-logon':
{
$login = \S::get('login');
$pass = \S::get('password');
$result = $users->logon($login, $pass);
if ( $result == 1 )
{
$user = $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 ( !$users->sendTwofaCode( (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 = $users->details($login);
self::finalize_admin_login(
$user,
$domain,
$cookie_name,
(bool)\S::get('remember')
);
header('Location: /admin/articles/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 = $users->verifyTwofaCode((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 = $users->details($pending['login']);
self::finalize_admin_login(
$user,
$domain,
$cookie_name,
$pending['remember'] ? true : false
);
header('Location: /admin/articles/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 (!$users->sendTwofaCode((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 ),
new \Domain\Languages\LanguagesRepository( $mdb ),
new \Domain\Layouts\LayoutsRepository( $mdb ),
new \Domain\Pages\PagesRepository( $mdb )
);
},
'ArticlesArchive' => function() {
global $mdb;
return new \admin\Controllers\ArticlesArchiveController(
new \Domain\Article\ArticleRepository( $mdb )
);
},
'Banners' => function() {
global $mdb;
return new \admin\Controllers\BannerController(
new \Domain\Banner\BannerRepository( $mdb ),
new \Domain\Languages\LanguagesRepository( $mdb )
);
},
'Settings' => function() {
global $mdb;
return new \admin\Controllers\SettingsController(
new \Domain\Settings\SettingsRepository( $mdb ),
new \Domain\Languages\LanguagesRepository( $mdb )
);
},
'ProductArchive' => function() {
global $mdb;
return new \admin\Controllers\ProductArchiveController(
new \Domain\Product\ProductRepository( $mdb )
);
},
// Alias dla starego modułu /admin/archive/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 ),
new \Domain\Languages\LanguagesRepository( $mdb )
);
},
'Filemanager' => function() {
return new \admin\Controllers\FilemanagerController();
},
'Users' => function() {
global $mdb;
return new \admin\Controllers\UsersController(
new \Domain\User\UserRepository( $mdb )
);
},
'Languages' => function() {
global $mdb;
return new \admin\Controllers\LanguagesController(
new \Domain\Languages\LanguagesRepository( $mdb )
);
},
'Layouts' => function() {
global $mdb;
return new \admin\Controllers\LayoutsController(
new \Domain\Layouts\LayoutsRepository( $mdb ),
new \Domain\Languages\LanguagesRepository( $mdb )
);
},
'Newsletter' => function() {
global $mdb;
return new \admin\Controllers\NewsletterController(
new \Domain\Newsletter\NewsletterRepository(
$mdb,
new \Domain\Settings\SettingsRepository( $mdb )
),
new \Domain\Newsletter\NewsletterPreviewRenderer()
);
},
'Scontainers' => function() {
global $mdb;
return new \admin\Controllers\ScontainersController(
new \Domain\Scontainers\ScontainersRepository( $mdb ),
new \Domain\Languages\LanguagesRepository( $mdb )
);
},
'ShopPromotion' => function() {
global $mdb;
return new \admin\Controllers\ShopPromotionController(
new \Domain\Promotion\PromotionRepository( $mdb )
);
},
'ShopCoupon' => function() {
global $mdb;
return new \admin\Controllers\ShopCouponController(
new \Domain\Coupon\CouponRepository( $mdb )
);
},
'ShopAttribute' => function() {
global $mdb;
return new \admin\Controllers\ShopAttributeController(
new \Domain\Attribute\AttributeRepository( $mdb ),
new \Domain\Languages\LanguagesRepository( $mdb )
);
},
'ShopPaymentMethod' => function() {
global $mdb;
return new \admin\Controllers\ShopPaymentMethodController(
new \Domain\PaymentMethod\PaymentMethodRepository( $mdb )
);
},
'ShopTransport' => function() {
global $mdb;
return new \admin\Controllers\ShopTransportController(
new \Domain\Transport\TransportRepository( $mdb ),
new \Domain\PaymentMethod\PaymentMethodRepository( $mdb )
);
},
'Pages' => function() {
global $mdb;
return new \admin\Controllers\PagesController(
new \Domain\Pages\PagesRepository( $mdb ),
new \Domain\Languages\LanguagesRepository( $mdb ),
new \Domain\Layouts\LayoutsRepository( $mdb )
);
},
'Integrations' => function() {
global $mdb;
return new \admin\Controllers\IntegrationsController(
new \Domain\Integrations\IntegrationsRepository( $mdb )
);
},
'ShopStatuses' => function() {
global $mdb;
return new \admin\Controllers\ShopStatusesController(
new \Domain\ShopStatus\ShopStatusRepository( $mdb )
);
},
'ShopProductSets' => function() {
global $mdb;
return new \admin\Controllers\ShopProductSetsController(
new \Domain\ProductSet\ProductSetRepository( $mdb )
);
},
'ShopProducer' => function() {
global $mdb;
return new \admin\Controllers\ShopProducerController(
new \Domain\Producer\ProducerRepository( $mdb ),
new \Domain\Languages\LanguagesRepository( $mdb )
);
},
'ShopCategory' => function() {
global $mdb;
return new \admin\Controllers\ShopCategoryController(
new \Domain\Category\CategoryRepository( $mdb ),
new \Domain\Languages\LanguagesRepository( $mdb )
);
},
'ShopProduct' => function() {
global $mdb;
return new \admin\Controllers\ShopProductController(
new \Domain\Product\ProductRepository( $mdb )
);
},
'ShopClients' => function() {
global $mdb;
return new \admin\Controllers\ShopClientsController(
new \Domain\Client\ClientRepository( $mdb )
);
},
];
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();
}
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 )
{
if ( method_exists( $controller, $action ) )
{
return $controller->$action();
}
if ( $moduleName === 'ShopAttribute' )
{
\S::alert( 'Nieprawidłowy adres url.' );
return false;
}
}
}
// 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,373 @@
<?php
namespace admin\controls;
class ShopProduct
{
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/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' => ( new \Domain\Languages\LanguagesRepository( $GLOBALS['mdb'] ) )->languagesList(),
'categories' => ( new \Domain\Category\CategoryRepository( $GLOBALS['mdb'] ) )->subcategories( null ),
'layouts' => self::layouts_for_product_edit( $mdb ),
'products' => \admin\factory\ShopProduct::products_list(),
'dlang' => \front\factory\Languages::default_language(),
'sets' => \shop\ProductSet::sets_list(),
'producers' => ( new \Domain\Producer\ProducerRepository( $mdb ) )->allProducers(),
'units' => ( new \Domain\Dictionaries\DictionariesRepository( $mdb ) ) -> allUnits(),
'user' => $user
] );
}
private static function layouts_for_product_edit( $db )
{
if ( class_exists( '\Domain\Layouts\LayoutsRepository' ) )
{
$rows = ( new \Domain\Layouts\LayoutsRepository( $db ) ) -> listAll();
return is_array( $rows ) ? $rows : [];
}
return [];
}
// 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/list/.',
'redirect_url' => '/admin/product_archive/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' ),
'apilo_enabled' => \admin\factory\Integrations::apilo_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 ),
'apilo_enabled' => \admin\factory\Integrations::apilo_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()
{
global $mdb;
return \Tpl::view( 'shop-product/product-combination', [
'product' => \admin\factory\ShopProduct::product_details( (int) \S::get( 'product_id' ) ),
'attributes' => ( new \Domain\Attribute\AttributeRepository( $mdb ) ) -> getAttributesListForCombinations(),
'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;
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,458 @@
# Changelog shopPRO
Logi zmian z migracji na Domain-Driven Architecture. Najnowsze na gorze.
---
## ver. 0.275 (2026-02-15) - ShopCategory
- **ShopCategory** - migracja `/admin/shop_category/*` na Domain + DI + nowe endpointy AJAX
- NOWE: `Domain\Category\CategoryRepository` (`sortTypes`, `subcategories`, `categoryDetails`, `categoryProducts`, `save`, `categoryDelete`, `saveCategoriesOrder`, `saveProductOrder`, `categoryTitle`)
- NOWE: `admin\Controllers\ShopCategoryController` (DI) z akcjami `list/view_list`, `edit/category_edit`, `save`, `delete/category_delete`, `products/category_products`, `category_url_browser`, `save_categories_order`, `save_products_order`, `cookie_categories`
- UPDATE: routing DI (`admin\Site`) rozszerzony o modul `ShopCategory`
- UPDATE: menu admin przepiete na kanoniczny URL `/admin/shop_category/list/`
- UPDATE: widoki `shop-category/*` - wydzielenie skryptow do `*-custom-script.php`, ujednolicone strzalki drzewa (`button + caret + aria-expanded`)
- UPDATE: AJAX drzewek przepiety z `/admin/ajax.php?a=*` na `/admin/shop_category/*`
- UPDATE: zaleznosci `ShopProduct` przepiete z `admin\factory\ShopCategory` na `Domain\Category\CategoryRepository`
- CLEANUP: usuniete legacy `autoload/admin/controls/class.ShopCategory.php`, `autoload/admin/factory/class.ShopCategory.php`, `autoload/admin/view/class.ShopCategory.php`
- CLEANUP: usuniety preload `class.ShopCategory.php` z `libraries/grid/config.php`
- TEST:
- NOWE: `tests/Unit/Domain/Category/CategoryRepositoryTest.php`
- NOWE: `tests/Unit/admin/Controllers/ShopCategoryControllerTest.php`
- Testy punktowe: **OK (16 tests, 72 assertions)**
---
## ver. 0.274 (2026-02-15) - ShopProduct mass_edit + UI trees
- **ShopProduct (mass_edit)** - migracja akcji masowej edycji na Domain + DI
- NOWE: `admin\Controllers\ShopProductController` (DI) z akcjami `mass_edit`, `mass_edit_save`, `get_products_by_category`
- UPDATE: routing DI (`admin\Site`) rozszerzony o modul `ShopProduct`
- UPDATE: `Domain\Product\ProductRepository` rozszerzone o metody `allProductsForMassEdit`, `getProductsByCategory`, `applyDiscountPercent` (+ aktualizacja cen kombinacji)
- CLEANUP: usuniete legacy akcje `mass_edit`, `mass_edit_save`, `get_products_by_category` z `admin\controls\ShopProduct`
- **ShopProduct mass_edit UI** - przebudowa widoku i skryptu
- UPDATE: `admin/templates/shop-product/mass-edit.php` przepiety na nowy partial JS `mass-edit-custom-script`
- NOWE: `admin/templates/shop-product/mass-edit-custom-script.php` (nestedSortable + iCheck + stabilizacja drzewka)
- UPDATE: `admin/templates/shop-product/subcategories-list.php` ujednolicone strzalki (button + caret)
- FIX: zaznaczenie kategorii w drzewku nie zaznacza automatycznie produktow na liscie
- **Pages / Articles UI** - ujednolicenie drzewek
- UPDATE: `/admin/pages/list/` - nowe strzalki drzewa + `aria-expanded` + odswiezanie stanu branch/leaf
- UPDATE: `/admin/articles/edit/*` (zakladka wyswietlania) - nowe strzalki i checkboxy (iCheck) dla drzewka stron
- **ShopClients** - migracja `/admin/shop_clients` na Domain + DI + nowe widoki
- NOWE: `Domain\Client\ClientRepository` (`listForAdmin`, `ordersForClient`, `totalsForClient`)
- NOWE: `admin\Controllers\ShopClientsController` (DI) z akcjami `list`, `details` + aliasy legacy `view_list`, `clients_details`
- UPDATE: routing DI (`admin\Site`) rozszerzony o modul `ShopClients`
- UPDATE: menu admin przepiete na kanoniczny URL `/admin/shop_clients/list/`
- UPDATE: widoki `shop-clients/view-list` i `shop-clients/clients-details` przepiete na `components/table-list`
- CLEANUP: usuniete legacy `autoload/admin/controls/class.ShopClients.php`, `autoload/admin/factory/class.ShopClients.php`
- TEST:
- NOWE: `tests/Unit/admin/Controllers/ShopProductControllerTest.php`
- NOWE: `tests/Unit/Domain/Client/ClientRepositoryTest.php`, `tests/Unit/admin/Controllers/ShopClientsControllerTest.php`
- UPDATE: `tests/Unit/Domain/Product/ProductRepositoryTest.php` (nowe przypadki dla mass_edit)
- UPDATE: `tests/bootstrap.php` (stub `S::normalize_decimal()`)
- Testy: **OK (361 tests, 1125 assertions)**
---
## ver. 0.273 (2026-02-15) - ShopProducer
- **ShopProducer** - migracja `/admin/shop_producer` na Domain + DI + nowe widoki
- NOWE: `Domain\Producer\ProducerRepository` (`listForAdmin`, `find`, `save`, `delete`, `allProducers`, `findForFrontend`, `producerProducts`, `allActiveIds`)
- NOWE: `admin\Controllers\ShopProducerController` (DI) z akcjami `list`, `edit`, `save`, `delete`
- UPDATE: modul `/admin/shop_producer/*` dziala na `components/table-list` i `components/form-edit` z zakladkami jezykowymi (Opis + SEO)
- UPDATE: routing i menu admin na kanoniczny URL `/admin/shop_producer/list/`
- UPDATE: `shop\Producer` przepiety na fasade do `Domain\Producer\ProducerRepository`
- UPDATE: `admin\factory\ShopProduct` - 2 wywolania `admin\factory\ShopTransport` przepiete na `Domain\Transport\TransportRepository`
- UPDATE: `admin\controls\ShopProduct` - usuniety fallback do `admin\factory\Layouts`
- CLEANUP: usuniete legacy `autoload/admin/controls/class.ShopProducer.php`, `admin/templates/shop-producer/list.php`, `admin/templates/shop-producer/edit.php`
- CLEANUP: usuniete 6 pustych factory facades: `admin\factory\Languages`, `admin\factory\Newsletter`, `admin\factory\Scontainers`, `admin\factory\ShopProducer`, `admin\factory\ShopTransport`, `admin\factory\Layouts`
- TEST: dodane `tests/Unit/Domain/Producer/ProducerRepositoryTest.php` i `tests/Unit/admin/Controllers/ShopProducerControllerTest.php`
- Testy: **OK (338 tests, 1063 assertions)**
---
## ver. 0.272 (2026-02-15) - ShopProductSets
- **ShopProductSets** - migracja `/admin/shop_product_sets` na Domain + DI + nowe widoki
- NOWE: `Domain\ProductSet\ProductSetRepository` (`listForAdmin`, `find`, `save`, `delete`, `allSets`, `allProductsMap`)
- NOWE: `admin\Controllers\ShopProductSetsController` (DI) z akcjami `list`, `edit`, `save`, `delete`
- UPDATE: modul `/admin/shop_product_sets/*` dziala na `components/table-list` i `components/form-edit` + Selectize multi-select produktow
- UPDATE: routing i menu admin na kanoniczny URL `/admin/shop_product_sets/list/`
- UPDATE: `shop\ProductSet` przepiety na fasade do `Domain\ProductSet\ProductSetRepository`
- CLEANUP: usuniete legacy `autoload/admin/controls/class.ShopProductSets.php`, `autoload/admin/factory/class.ShopProductSet.php`, `admin/templates/shop-product-sets/view-list.php`, `admin/templates/shop-product-sets/set-edit.php`
- TEST: dodane `tests/Unit/Domain/ProductSet/ProductSetRepositoryTest.php` i `tests/Unit/admin/Controllers/ShopProductSetsControllerTest.php`
- Testy: **OK (324 tests, 1000 assertions)**
---
## ver. 0.271 (2026-02-14) - ShopAttribute
- **ShopAttribute** - migracja `/admin/shop_attribute` na Domain + DI + nowe widoki
- NOWE: `Domain\Attribute\AttributeRepository` (`listForAdmin`, `findAttribute`, `saveAttribute`, `deleteAttribute`, `findValues`, `saveValues`, `saveLegacyValues`, `valueDetails`)
- NOWE: `admin\Controllers\ShopAttributeController` (DI) z akcjami `list`, `edit`, `save`, `delete`, `values`, `values_save`, `value_row_tpl`
- UPDATE: modul `/admin/shop_attribute/*` dziala na `components/table-list` i `components/form-edit`
- UPDATE: nowy edytor wartosci cechy (`values-edit`) z walidacja serwerowa i stabilnym `row_key` (bez indeksow do wyboru domyslnej wartosci)
- UPDATE: routing i menu admin na kanoniczny URL `/admin/shop_attribute/list/` (bez aliasow legacy)
- UPDATE: przepiecie zaleznosci kombinacji produktu (`admin\controls\ShopProduct`, `admin\factory\ShopProduct`, `admin/templates/shop-product/product-combination.php`) na `Domain\Attribute\AttributeRepository` i `shop\ProductAttribute`
- CLEANUP: usuniete legacy `autoload/admin/controls/class.ShopAttribute.php`, `autoload/admin/factory/class.ShopAttribute.php`, `autoload/admin/view/class.ShopAttribute.php`, `admin/templates/shop-attribute/_partials/value.php`
- TEST: dodane `tests/Unit/Domain/Attribute/AttributeRepositoryTest.php` i `tests/Unit/admin/Controllers/ShopAttributeControllerTest.php`
- Testy: **OK (312 tests, 948 assertions)**
---
## ver. 0.270 (2026-02-14) - Apilo payment/status sync hardening
- **Shop/Order + Apilo** - utwardzenie synchronizacji platnosci i statusow zamowien
- FIX: `shop\Order::set_as_paid()` wysyla do Apilo mapowany typ platnosci (`payment_method_id` -> `apilo_payment_type_id`) zamiast stalego `type = 1`
- NOWE: retry queue dla chwilowej niedostepnosci Apilo (`temp/apilo-sync-queue.json`) dla sync platnosci i statusu
- NOWE: `shop\Order::process_apilo_sync_queue()` przetwarza zalegle syncy
- UPDATE: `cron.php` uruchamia przetwarzanie kolejki sync Apilo przy aktywnej integracji
- UPDATE: rozszerzone logowanie debug (`logs/apilo.txt`) o HTTP code i bledy cURL dla sync platnosci/statusu
- Testy: **OK (300 tests, 895 assertions)**
---
## ver. 0.269 (2026-02-14) - ShopTransport
- **ShopTransport** - migracja `/admin/shop_transport` na Domain + DI + nowe widoki
- NOWE: `Domain\Transport\TransportRepository` (`listForAdmin`, `find`, `save`, `allActive`, `allForAdmin`, `findActiveById`, `getTransportCost`, `lowestTransportPrice`, `getApiloCarrierAccountId`)
- NOWE: `admin\Controllers\ShopTransportController` (DI) z akcjami `list`, `edit`, `save`
- NOWE: widoki `shop-transport/transports-list.php` i `shop-transport/transport-edit.php` + `transport-edit-custom-script.php`
- UPDATE: routing i menu admin na kanoniczny URL `/admin/shop_transport/list/`
- UPDATE: `admin\factory\ShopTransport`, `front\factory\ShopTransport` przepiete na nowe repozytorium
- FIX: `save()` return type `?int` zamiast `int|bool` (spojnosc z PaymentMethod)
- FIX: `toSwitchValue()` helper zamiast `=== 'on'` (obsluga '1', 'on', 'true', 'yes')
- FIX: `\S::delete_dir()` przeniesione z repozytorium do kontrolera (DDD)
- FIX: Medoo `select()` syntax - ORDER w WHERE zamiast 4-arg form
- CLEANUP: usuniete legacy `autoload/admin/controls/class.ShopTransport.php`, `autoload/admin/view/class.ShopTransport.php`, `admin/templates/shop-transport/view-list.php`
- FIX: `transports-list.php` - zmienna `'viewModel'` zmieniona na `'list'` (zgodnie z `table-list.php` komponentem)
- Testy: **OK (300 tests, 895 assertions)**
---
## ver. 0.268 (2026-02-14) - ShopPaymentMethod + Apilo token keepalive
- **ShopPaymentMethod** - migracja `/admin/shop_payment_method` na Domain + DI + nowe widoki
- NOWE: `Domain\PaymentMethod\PaymentMethodRepository` (`listForAdmin`, `find`, `save`, `allActive`, `allForAdmin`, `findActiveById`, `isActive`, `getApiloPaymentTypeId`, `forTransport`)
- NOWE: `admin\Controllers\ShopPaymentMethodController` (DI) z akcjami `list`, `edit`, `save`
- NOWE: widoki `shop-payment-method/payment-methods-list.php` i `shop-payment-method/payment-method-edit.php`
- UPDATE: routing i menu admin na kanoniczny URL `/admin/shop_payment_method/list/`
- UPDATE: `admin\controls\ShopTransport`, `front\factory\ShopPaymentMethod`, `shop\PaymentMethod` przepiete na nowe repozytorium
- CLEANUP: usuniete legacy `autoload/admin/controls/class.ShopPaymentMethod.php`, `autoload/admin/factory/class.ShopPaymentMethod.php`, `autoload/admin/view/class.ShopPaymentMethod.php`, `admin/templates/shop-payment-method/view-list.php`
- **Integrations/Apilo** - stabilizacja tokenu i lepszy feedback
- NOWE: automatyczne odswiezanie tokenu Apilo przed wygasnieciem (`apiloKeepalive`, refresh lead time)
- UPDATE: cron uruchamia keepalive i odswieza konfiguracje Apilo
- UPDATE: bardziej szczegolowe komunikaty bledow dla przyciskow integracji Apilo (co zrobic dalej)
- Testy: **OK (280 tests, 828 assertions)**
---
## ver. 0.267 (2026-02-14) - ShopStatuses
- **ShopStatuses** - migracja `/admin/shop_statuses` na Domain + DI + nowe widoki
- NOWE: `Domain\ShopStatus\ShopStatusRepository` (`listForAdmin`, `find`, `save`, `getApiloStatusId`, `getByIntegrationStatusId`, `allStatuses`)
- NOWE: `admin\Controllers\ShopStatusesController` (DI) z akcjami `list`, `edit`, `save` (bez aliasow legacy)
- NOWE: typ pola `FormFieldType::COLOR` + `FormField::color()` + `FormFieldRenderer::renderColor()` (color picker HTML5 zsynchronizowany z polem tekstowym)
- UPDATE: modul `/admin/shop_statuses/*` dziala na `components/table-list` i `components/form-edit`
- UPDATE: `front\factory\ShopStatuses` jako fasada delegujaca do `Domain\ShopStatus\ShopStatusRepository`
- UPDATE: menu admin przepiete na kanoniczny URL `/admin/shop_statuses/list/`
- CLEANUP: usuniete legacy `autoload/admin/controls/class.ShopStatuses.php`, `autoload/admin/factory/class.ShopStatuses.php`
- UWAGA: statusy maja ID od 0 - kluczowe dla walidacji (find/save uzywaja `$id < 0`)
- Testy: **OK (254 tests, 736 assertions)**
---
## ver. 0.266 (2026-02-13) - ShopCoupon
- **ShopCoupon** - migracja `/admin/shop_coupon` na Domain + DI + nowe widoki
- NOWE: `Domain\Coupon\CouponRepository` (`listForAdmin`, `find`, `save`, `delete`, `categoriesTree`)
- NOWE: `admin\Controllers\ShopCouponController` (DI) z akcjami `list`, `edit`, `save`, `delete`
- UPDATE: kompatybilnosc aliasow legacy (`view_list`, `coupon_edit`, `coupon_save`, `coupon_delete`) obslugiwana przez nowy kontroler
- UPDATE: modul `/admin/shop_coupon/*` dziala na `components/table-list` i `components/form-edit`
- NOWE: widoki/partiale `shop-coupon/coupons-list`, `shop-coupon/coupon-edit-new`, `shop-coupon/coupon-categories-selector`, `shop-coupon/coupon-categories-tree`, `shop-coupon/coupon-edit-custom-script`
- CLEANUP: usuniete legacy `autoload/admin/controls/class.ShopCoupon.php`, `autoload/admin/factory/class.ShopCoupon.php`, `admin/templates/shop-coupon/view-list.php`, `admin/templates/shop-coupon/coupon-edit.php`
- UPDATE: menu admin przepiete na kanoniczny URL `/admin/shop_coupon/list/`
- FIX: ujednolicone UI drzewek i checkboxow miedzy kuponami i layoutami
- Testy: **OK (235 tests, 682 assertions)**
---
## ver. 0.265 (2026-02-13) - ShopPromotion poprawki
- **ShopPromotion** - stabilizacja po migracji
- UPDATE: dodane `date_from` w `Domain\Promotion\PromotionRepository` (save/find/list/sort)
- UPDATE: `admin\Controllers\ShopPromotionController` rozszerzony o pole `Data od` na formularzu i kolumne `Data od` na liscie
- UPDATE: `shop\Promotion::get_active_promotions()` filtruje aktywnosc po `date_from` i `date_to`
- FIX: zapis edycji promocji nie tworzy nowego rekordu (hidden `id` + fallback `id` z URL)
- TEST: rozszerzono `PromotionRepositoryTest` o asercje `date_from`
- Testy: **OK (222 tests, 614 assertions)**
---
## ver. 0.264 (2026-02-13) - ShopPromotion
- **ShopPromotion** - migracja `/admin/shop_promotion` na Domain + DI + nowe widoki
- NOWE: `Domain\Promotion\PromotionRepository` (`listForAdmin`, `find`, `save`, `delete`, `categoriesTree`, invalidacja cache aktywnych promocji)
- NOWE: `admin\Controllers\ShopPromotionController` (DI) z akcjami `list`, `edit`, `save`, `delete`
- UPDATE: routing DI (`admin\Site`) rozszerzony o modul `ShopPromotion`
- UPDATE: modul `/admin/shop_promotion/*` dziala na `components/table-list` i `components/form-edit`
- NOWE: widoki/partiale `shop-promotion/promotions-list`, `shop-promotion/promotion-edit`, `shop-promotion/promotion-categories-selector`, `shop-promotion/promotion-categories-tree`, `shop-promotion/promotion-edit-custom-script`
- CLEANUP: usuniete legacy `autoload/admin/controls/class.ShopPromotion.php`, `autoload/admin/factory/class.ShopPromotion.php`, `admin/templates/shop-promotion/view-list.php`
- UPDATE: menu admin przepiete na kanoniczny URL `/admin/shop_promotion/list/`
- Testy: **OK (222 tests, 609 assertions)**
---
## ver. 0.263 (2026-02-13) - Integrations + cleanup Sellasist/Baselinker
- NOWE: `Domain\Integrations\IntegrationsRepository` (settings Apilo/ShopPRO, OAuth, product linking, API fetch)
- NOWE: `admin\Controllers\IntegrationsController` (DI) dla akcji Apilo i ShopPRO
- UPDATE: `admin\factory\Integrations` jako fasada delegujaca do repozytorium
- **CLEANUP: usunieto integracje Sellasist i Baselinker z calego projektu:**
- Usuniete klasy: `admin\controls\Integrations`, `admin\controls\Baselinker`, `admin\factory\Baselinker`, `front\factory\Shop`, `shop\ShopStatus`
- Usuniete szablony: `integrations/sellasist-settings.php`, `integrations/baselinker-settings.php`, `admin/templates/baselinker/`
- Wyczyszczone referencje w: `cron.php`, `cron/cron-xml.php`, `shop\Order`, kontrolery/factory/front Shop*
- Testy: **OK (212 tests, 577 assertions)**
---
## ver. 0.262 (2026-02-13) - Pages
- NOWE: `Domain\Pages\PagesRepository` (CRUD menu/stron, drzewo stron, sortowanie, SEO)
- NOWE: `admin\Controllers\PagesController` (DI) dla akcji menu/page/AJAX
- UPDATE: widoki `admin/templates/pages/*` przepiete na dane z kontrolera/repozytorium
- UPDATE: endpointy AJAX przepiete z `admin/ajax.php?a=*` na `/admin/pages/*`
- CLEANUP: usuniete legacy `controls/Pages`, `view/Pages`, `factory/Pages`, `ajax/pages.php`
- Testy: **OK (186 tests, 478 assertions)**
---
## ver. 0.261 (2026-02-13) - Articles (dalsza refaktoryzacja)
- UPDATE: `Domain\Article\ArticleRepository` rozszerzone o metody UI/admin i `saveFilesOrder()`
- UPDATE: `admin\Controllers\ArticlesController` obsluguje AJAX: `article_image_alt_change`, `article_file_name_change`, `article_image_delete`, `article_file_delete`, `filesOrderSave`
- UPDATE: lista artykulow nie korzysta juz z `admin\factory\Articles::article_pages()`
- UPDATE: widok edycji przepiety z `/admin/ajax.php` na `/admin/articles/*`
- UPDATE: drag&drop sortowania listy zalacznikow
- CLEANUP: usuniete `autoload/admin/view/class.Articles.php` i `admin/ajax/articles.php`
- Testy: **OK (178 tests, 443 assertions)**
---
## ver. 0.260 (2026-02-12) - ArticlesArchive
- NOWE: `admin\Controllers\ArticlesArchiveController` (DI)
- UPDATE: `Domain\Article\ArticleRepository` rozszerzone o `listArchivedForAdmin()`, `restore()`, `deletePermanently()`
- UPDATE: `/admin/articles_archive/view_list/` migrowane na `components/table-list`
- CLEANUP: usuniete legacy `controls/ArticlesArchive`, `factory/ArticlesArchive`, `view/ArticlesArchive`
- Testy: **OK (165 tests, 424 assertions)**
---
## ver. 0.259 (2026-02-12) - Scontainers
- NOWE: `Domain\Scontainers\ScontainersRepository` (listForAdmin, find, save, delete, detailsForLanguage)
- NOWE: `admin\Controllers\ScontainersController` (DI)
- UPDATE: `/admin/scontainers/*` migrowane na `components/table-list` i `components/form-edit`
- UPDATE: `admin\factory\Scontainers` i `front\factory\Scontainers` jako fasady
- CLEANUP: usuniete `controls/Scontainers`, `view/Scontainers`
- Testy: **OK (158 tests, 397 assertions)**
---
## ver. 0.258 (2026-02-12) - Newsletter (stabilizacja)
- UPDATE: tymczasowo wylaczono flow `prepare/send/preview` (wymaga przebudowy)
- UPDATE: tymczasowo wylaczono modul `Szablony uzytkownika`
- UPDATE: aktywna obsluga tylko szablonow administracyjnych (`is_admin = 1`)
- CLEANUP: usuniete nieuzywane widoki `prepare.php`, `preview.php`, `email-templates-user.php`
---
## ver. 0.257 (2026-02-12) - Newsletter
- NOWE: `Domain\Newsletter\NewsletterRepository` (subskrybenci, szablony, ustawienia, kolejka wysylki)
- NOWE: `Domain\Newsletter\NewsletterPreviewRenderer` (render podgladu)
- NOWE: `admin\Controllers\NewsletterController` (DI)
- UPDATE: `/admin/newsletter/*` migrowane na `components/table-list` i `components/form-edit`
- UPDATE: `admin\factory\Newsletter` jako fasada; `front\factory\Newsletter` bez `admin\view\Newsletter`
- CLEANUP: usuniete `controls/Newsletter`, `view/Newsletter`
- Testy: **OK (150 tests, 372 assertions)**
---
## ver. 0.256 (2026-02-12) - Layouts
- NOWE: `Domain\Layouts\LayoutsRepository` (find, save, delete, listForAdmin, menusWithPages, categoriesTree)
- NOWE: `admin\Controllers\LayoutsController` (DI)
- UPDATE: lista `/admin/layouts/view_list/` migrowana na `components/table-list`
- UPDATE: widok `layouts/layout-edit` korzysta z danych z repozytorium
- NOWE: partial `admin/templates/layouts/subcategories-list.php`
- UPDATE: `Domain\Languages\LanguagesRepository::defaultLanguageId()` jako wspolna metoda
- UPDATE: `ArticlesController` korzysta z `LayoutsRepository` (DI)
- CLEANUP: usuniete `controls/Layouts`, `view/Layouts`; `factory/Layouts` jako fasada
- Testy: **OK (141 tests, 336 assertions)**
---
## ver. 0.255 (2026-02-12) - Languages DI cleanup
- UPDATE: SettingsController, BannerController, DictionariesController, ArticlesController pobieraja liste jezykow przez `Domain/Languages/LanguagesRepository` (DI)
- UPDATE: router DI przekazuje `LanguagesRepository` do kontrolerow
- UPDATE: legacy `admin/controls`, `admin/factory/Shop*` przepiete na `LanguagesRepository`
- FIX: `admin/factory/class.Languages.php` poprawione na `<?php` (short_open_tag=Off)
- Testy: **OK (130 tests, 303 assertions)**
---
## ver. 0.254 (2026-02-12) - Languages
- NOWE: `Domain\Languages\LanguagesRepository` (languages + translations CRUD/list)
- NOWE: `admin\Controllers\LanguagesController` (DI)
- UPDATE: widoki `languages/*` migrowane na `components/table-list` i `components/form-edit`
- CLEANUP: usuniete `controls/Languages`, `view/Languages`; `factory/Languages` jako fasada
- Testy: **OK (130 tests, 301 assertions)**
---
## ver. 0.253 (2026-02-12) - Users
- NOWE: `Domain\User\UserRepository` (CRUD, logon, 2FA, checkLogin)
- NOWE: `admin\Controllers\UsersController` (DI: view_list, user_edit, user_save, user_delete, login_form, twofa)
- UPDATE: `admin\factory\Users` jako fasada; `admin/ajax/users.php` oparty o `UserRepository`
- UPDATE: widoki users migrowane na `components/table-list` i `components/form-edit`
- UPDATE: walidacja warunkowa: `twofa_email` wymagany gdy `twofa_enabled = 1`
- CLEANUP: usuniete `controls/Users`, `factory/Users`, `view/Users`
- Testy: **OK (119 tests, 256 assertions)**
---
## ver. 0.252 (2026-02-10) - ProductArchive + Filemanager
- UPDATE: `ProductArchiveController` i szablony przepiete na `components/table-list`
- UPDATE: CSS/JS dla list wydzielone do osobnych widoków `*-custom-script.php`
- NOWE: `admin\Controllers\FilemanagerController` - przepieto filemanager na nowy routing
- FIX: naprawiono błąd `Invalid Key` w filemanagerze
- CLEANUP: usunieto legacy `controls/Archive`, `controls/Filemanager`, `view/FileManager`, stare szablony
- RENAME: `admin/templates/product_archive/``admin/templates/product-archive/`
- Testy: **OK (82 tests, 181 assertions)**
---
## ver. 0.251 (2026-02-09) - Dictionaries
- NOWE: `Domain\Dictionaries\DictionariesRepository` (listForAdmin, find, save, delete, allUnits)
- NOWE: `admin\Controllers\DictionariesController` (lista + formularz na nowych komponentach)
- UPDATE: migracja słowników na `components/table-list` i `components/form-edit`
- FIX: obsługa `lang_id` jako string (`pl`, `en`) w zapisie tłumaczeń
- CLEANUP: usunięto legacy klasy Dictionaries (`admin\controls`, `admin\factory`, `front\factory`)
- Testy: **OK (82 tests, 181 assertions)**
---
## ver. 0.250 (2026-02-09) - Settings (refaktoryzacja)
- UPDATE: `Domain\Settings\SettingsRepository` ma bezpośredni dostęp do DB (bez delegacji do `admin\factory\Settings`)
- UPDATE: przepięto użycia `admin\factory\Settings` na `Domain\Settings\SettingsRepository`
- CLEANUP: usunięto legacy klasy Settings (`factory`, `controls`, `view`)
- Testy: **OK (82 tests, 181 assertions)**
---
## ver. 0.246 (2026-02-07) - Articles legacy cleanup
- CLEANUP: usunięto `autoload/admin/controls/class.Articles.php`
- UPDATE: `admin\Controllers\ArticlesController::galleryOrderSave()` uzywa `ArticleRepository::saveGalleryOrder()`
- FIX: sortowanie list admin po reloadzie - `RewriteRule` dla `/admin/...` ma `QSA`
- FIX: generator `\S::htacces()` komentuje dyrektywy `AddHandler|SetHandler|ForceType`
- Testy: **OK (65 tests, 131 assertions)**
---
## ver. 0.245 (2026-02-06) - Articles::archive
- UPDATE: `Domain\Article\ArticleRepository` - dodano `archive()` (status = -1)
- UPDATE: `admin\Controllers\ArticlesController` - nowa akcja `delete()` z DI
- UPDATE: `admin\factory\Articles::articles_set_archive()` deleguje do repozytorium
- Testy: **OK (59 tests, 123 assertions)**
---
## ver. 0.244 (2026-02-06) - Articles::save
- UPDATE: `Domain\Article\ArticleRepository` - dodano `save()` z prywatnych helperow
- UPDATE: `admin\Controllers\ArticlesController` - nowa akcja `save()` z DI
- UPDATE: `tests/bootstrap.php` - dodano stub `S::seo()`
- Testy: **OK (57 tests, 119 assertions)**
---
## ver. 0.243 (2026-02-06) - Articles cleanup
- UPDATE: `Domain\Article\ArticleRepository` - dodano `deleteNonassignedImages()` i `deleteNonassignedFiles()`
- UPDATE: `admin\Controllers\ArticlesController::edit()` uses repository cleanup methods
- Testy: **OK (50 tests, 95 assertions)**
---
## ver. 0.242 (2026-02-06) - Articles::edit
- NOWE: `Domain\Article\ArticleRepository` (find: artykuł + relacje)
- UPDATE: `admin\Controllers\ArticlesController` - konstruktor DI + `edit()` używa repozytorium
- UPDATE: `admin\factory\Articles::article_details()` deleguje do repozytorium
- Testy: **OK (48 tests, 91 assertions)**
---
## ver. 0.241 (2026-02-06) - ProductArchive
- NOWE: `admin\Controllers\ProductArchiveController` (DI)
- NOWE: `ProductRepository::archive()`, `unarchive()`
- RENAME: `admin/templates/archive/``admin/templates/product_archive/`
- FIX: SQL w `ajax_products_list_archive()` (puste wyszukiwanie + brak `archive = 1`)
- Testy: **OK (50 tests, 95 assertions)**
---
## ver. 0.240 (2026-02-05) - Settings + Cache
- NOWE: `Domain\Settings\SettingsRepository` (fasada → factory)
- NOWE: `Domain\Cache\CacheRepository` (dirs + Redis)
- NOWE: `admin\Controllers\SettingsController` (DI: clearCache, save, view)
- FIX: Brakujący `id="content"` w main-layout.php
- FIX: `persist_edit = true` w settings.php
- Testy: **OK (29 tests, 60 assertions)**
---
## ver. 0.239 (2026-02-05) - Banner + Product
- NOWE: `Domain\Banner\BannerRepository` (find, delete, save)
- NOWE: `admin\Controllers\BannerController` - pierwszy kontroler z DI
- NOWE: Router z mapą `$newControllers` + fallback na stare kontrolery
- NOWE: Autoloader PSR-4 fallback w 9 entry pointach
- Zmigrowano: `get_product_price()``ProductRepository::getPrice()`
- Zmigrowano: `get_product_name()``ProductRepository::getName()`
- Testy: **OK (15 tests, 31 assertions)**
---
## ver. 0.238 (2025-02-05) - Product Repository
- NOWE: `Domain\Product\ProductRepository` - pierwsza klasa w nowej architekturze
- NOWE: Dependency Injection zamiast `global $mdb`
- NOWE: Testy jednostkowe (5 testów, 100% pokrycie)
- Zmigrowano: `get_product_quantity()``ProductRepository::getQuantity()`
- Kompatybilność: Stara klasa `shop\Product` działa jako fasada
---
## ver. 0.237 (2025-02-05) - System cache produktów
- Automatyczne czyszczenie cache produktu po aktualizacji przez CRON
- AJAX dla przycisku "Wyczyść cache" w panelu admin
- Metody `delete()` i `deletePattern()` w CacheHandler
- Metoda `clear_product_cache()` w klasie S
---
*Dokument aktualizowany: 2026-02-14*

View File

@@ -0,0 +1,630 @@
# 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 |
| apilo_product_id | ID produktu w Apilo |
| apilo_product_name | Nazwa produktu w Apilo |
**Używane w:** `Domain\Product\ProductRepository`, `admin\factory\ShopProduct`, `admin\Controllers\ShopProductController`
## 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()`, `Domain\Product\ProductRepository::getProductsByCategory()`
**Aktualizacja 2026-02-15 (ver. 0.274):** akcje `/admin/shop_product/mass_edit/*` korzystają z `Domain\Product\ProductRepository` przez `admin\Controllers\ShopProductController`.
## pp_shop_categories
Kategorie sklepu.
| Kolumna | Opis |
|---------|------|
| id | PK |
| parent_id | FK do kategorii nadrzednej (NULL dla root) |
| status | 1 = aktywna, 0 = nieaktywna |
| o | Kolejnosc wyswietlania |
| sort_type | Typ sortowania produktow w kategorii |
| view_subcategories | Czy wyswietlac podkategorie |
**Uzywane w:** `Domain\Category\CategoryRepository`, `admin\Controllers\ShopCategoryController`.
## pp_shop_categories_langs
Tlumaczenia kategorii (per jezyk).
| Kolumna | Opis |
|---------|------|
| category_id | FK do pp_shop_categories |
| lang_id | ID jezyka (np. pl, en) |
| title | Nazwa kategorii |
| text | Opis kategorii |
| text_hidden | Rozwiniecie opisu kategorii |
| seo_link | Link SEO kategorii |
| meta_title | Meta title |
| meta_description | Meta description |
| meta_keywords | Meta keywords |
| noindex | Flaga noindex |
| category_title | Naglowek H1 kategorii |
| additional_text | Dodatkowy tekst nad lista produktow |
**Uzywane w:** `Domain\Category\CategoryRepository`, `admin\Controllers\ShopCategoryController`.
**Aktualizacja 2026-02-15 (ver. 0.275):** modul `/admin/shop_category/*` korzysta z `Domain\Category\CategoryRepository` przez `admin\Controllers\ShopCategoryController`; usunieto legacy `admin\controls/factory/view\ShopCategory`.
## pp_shop_orders
Zamówienia sklepu (źródło danych dla list i szczegółów klientów w panelu admin).
| Kolumna | Opis |
|---------|------|
| id | PK |
| client_id | FK do `pp_shop_clients` (NULL dla gościa) |
| client_name | Imię klienta z zamówienia |
| client_surname | Nazwisko klienta z zamówienia |
| client_email | E-mail klienta z zamówienia |
| client_phone | Telefon klienta |
| client_city | Miasto klienta |
| summary | Wartość zamówienia |
| date_order | Data złożenia zamówienia |
| payment_method | Nazwa metody płatności |
| transport | Nazwa transportu |
| message | Wiadomość klienta |
**Używane w:** `Domain\Client\ClientRepository::listForAdmin()`, `Domain\Client\ClientRepository::ordersForClient()`, `Domain\Client\ClientRepository::totalsForClient()`.
**Aktualizacja 2026-02-15 (ver. 0.274):** moduł `/admin/shop_clients/*` korzysta z `Domain\Client\ClientRepository` przez `admin\Controllers\ShopClientsController`.
## 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\Controllers\ArticlesArchiveController`, `Domain\Article\ArticleRepository::find()`, `Domain\Article\ArticleRepository::listArchivedForAdmin()`
## pp_articles_pages
Strony artykułów.
| Kolumna | Opis |
|---------|------|
| article_id | FK do pp_articles |
| page_id | FK do strony (pp_pages) |
| o | Kolejność |
**Używane w:** `Domain\Article\ArticleRepository::find()`, `Domain\Article\ArticleRepository::deleteNonassignedImages()`
## pp_articles_langs
Tłumaczenia artykułów.
| Kolumna | Opis |
|---------|------|
| article_id | FK do pp_articles |
| lang_id | ID języka (np. 'pl') |
| title | Tytuł artykułu |
| seo_link | Link SEO artykułu |
**Używane w:** `Domain\Article\ArticleRepository::find()`, `Domain\Article\ArticleRepository::deleteNonassignedFiles()`
## pp_articles_images
Zdjęcia artykułów.
| Kolumna | Opis |
|---------|------|
| article_id | FK do pp_articles |
| src | Ścieżka do pliku |
| o | Kolejność |
| id | PK (używane też do sortowania DESC) |
**Używane w:** `Domain\Article\ArticleRepository::find()`
## pp_articles_files
Pliki artykułów.
| Kolumna | Opis |
|---------|------|
| id | PK |
| article_id | FK do pp_articles |
| src | Ścieżka do pliku |
| name | Nazwa wyświetlana załącznika (opcjonalna) |
| to_delete | Flaga miękkiego usuwania (0/1) |
| o | Kolejność załączników (używana przez sortowanie drag&drop w adminie) |
**Używane w:** `Domain\Article\ArticleRepository::find()`, `Domain\Article\ArticleRepository::saveFilesOrder()`
## 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`
## pp_users
Uzytkownicy panelu administratora.
| Kolumna | Opis |
|---------|------|
| id | PK |
| login | Login / e-mail uzytkownika |
| password | Hash hasla (legacy: md5) |
| status | Status konta: 1 = aktywny, 0 = zablokowany |
| admin | Flaga dostepu do panelu admin |
| error_logged_count | Licznik nieudanych logowan |
| last_logged | Data ostatniego poprawnego logowania |
| last_error_logged | Data ostatniej nieudanej proby logowania |
| twofa_enabled | Czy wlaczone 2FA (0/1) |
| twofa_email | E-mail do wysylki kodu 2FA |
| twofa_code_hash | Hash aktualnego kodu 2FA |
| twofa_expires_at | Data waznosci kodu 2FA |
| twofa_sent_at | Data ostatniej wysylki kodu 2FA |
| twofa_failed_attempts | Liczba nieudanych prob 2FA |
**Uzywane w:** `Domain\User\UserRepository`, `admin\Controllers\UsersController`, `admin\factory\Users`
**Aktualizacja 2026-02-12:** uzycia `pp_users` sa prowadzone przez `Domain\\User\\UserRepository` (legacy `admin\\factory\\Users` usunieto).
## pp_langs
Jezyki panelu i frontendu.
| Kolumna | Opis |
|---------|------|
| id | PK (2-literowe ID jezyka, np. pl, en) |
| name | Nazwa jezyka |
| status | 1 = aktywny, 0 = nieaktywny |
| start | 1 = domyslny jezyk |
| o | Kolejnosc |
**Uzywane w:** `Domain\\Languages\\LanguagesRepository`, `admin\\Controllers\\LanguagesController`, `front\\factory\\Languages`
## pp_langs_translations
Slownik tlumaczen panelu/frontendu.
| Kolumna | Opis |
|---------|------|
| id | PK |
| text | Klucz/tekst bazowy |
| <lang_id> | Kolumny dynamiczne per jezyk (np. pl, en) |
**Uzywane w:** `Domain\\Languages\\LanguagesRepository`, `admin\\Controllers\\LanguagesController`, `front\\factory\\Languages`
**Aktualizacja 2026-02-12:** modul jezykow i tlumaczen (`pp_langs`, `pp_langs_translations`) obslugiwany przez `Domain\\Languages\\LanguagesRepository`.
## pp_layouts
Szablony layoutow (HTML/CSS/JS + flagi domyslne).
| Kolumna | Opis |
|---------|------|
| id | PK |
| name | Nazwa szablonu |
| html | Kod HTML |
| css | Kod CSS |
| js | Kod JS |
| m_html | Kod HTML mobilny |
| m_css | Kod CSS mobilny |
| m_js | Kod JS mobilny |
| status | Domyslny layout stron (0/1) |
| categories_default | Domyslny layout kategorii (0/1) |
**Uzywane w:** `Domain\\Layouts\\LayoutsRepository`, `admin\\Controllers\\LayoutsController`, `front\\factory\\Layouts`
## pp_layouts_pages
Przypisanie layoutow do stron CMS.
| Kolumna | Opis |
|---------|------|
| layout_id | FK do pp_layouts |
| page_id | FK do pp_pages |
**Uzywane w:** `Domain\\Layouts\\LayoutsRepository`, `front\\factory\\Layouts`
## pp_layouts_categories
Przypisanie layoutow do kategorii sklepu.
| Kolumna | Opis |
|---------|------|
| layout_id | FK do pp_layouts |
| category_id | FK do pp_shop_categories |
**Uzywane w:** `Domain\\Layouts\\LayoutsRepository`, `front\\factory\\Layouts`
**Aktualizacja 2026-02-12 (ver. 0.256):** modul `/admin/layouts` korzysta z `Domain\\Layouts\\LayoutsRepository` (DI kontroler + fasada legacy).
## pp_newsletter
Adresy e-mail zapisane do newslettera.
| Kolumna | Opis |
|---------|------|
| id | PK |
| email | Adres e-mail subskrybenta |
| hash | Hash potwierdzenia/wypisu |
| status | 1 = potwierdzony, 0 = oczekujacy |
**Uzywane w:** `Domain\\Newsletter\\NewsletterRepository`, `front\\factory\\Newsletter`
## pp_newsletter_send
Kolejka wysylki newslettera.
| Kolumna | Opis |
|---------|------|
| id | PK |
| email | Adres docelowy |
| dates | Zakres dat artykulow (tekst) |
| id_template | FK do `pp_newsletter_templates` (NULL gdy brak szablonu) |
**Uzywane w:** `Domain\\Newsletter\\NewsletterRepository`, `front\\factory\\Newsletter::newsletter_send()`
## pp_newsletter_templates
Szablony tresci e-maili (uzytkownik + administracyjne/systemowe).
| Kolumna | Opis |
|---------|------|
| id | PK |
| name | Nazwa/klucz szablonu |
| text | Tresc HTML szablonu |
| is_admin | 1 = szablon administracyjny/systemowy, 0 = szablon uzytkownika |
**Uzywane w:** `Domain\\Newsletter\\NewsletterRepository`, `admin\\Controllers\\NewsletterController`, `front\\factory\\Newsletter`
**Aktualizacja 2026-02-12 (ver. 0.257):** modul `/admin/newsletter` korzysta z `Domain\\Newsletter\\NewsletterRepository` (DI kontroler + fasada legacy).
## pp_scontainers
Kontenery statyczne (modul /admin/scontainers).
| Kolumna | Opis |
|---------|------|
| id | PK |
| status | 1 = aktywny, 0 = nieaktywny |
| show_title | 1 = pokaz tytul, 0 = ukryj tytul |
**Uzywane w:** `Domain\Scontainers\ScontainersRepository`, `admin\Controllers\ScontainersController`, `front\factory\Scontainers`
## pp_scontainers_langs
Tlumaczenia kontenerow statycznych (per jezyk).
| Kolumna | Opis |
|---------|------|
| id | PK |
| container_id | FK do pp_scontainers |
| lang_id | ID jezyka (np. pl, en) |
| title | Tytul kontenera |
| text | Tresc HTML kontenera |
**Uzywane w:** `Domain\Scontainers\ScontainersRepository`, `front\factory\Scontainers`
**Aktualizacja 2026-02-12 (ver. 0.259):** modul `/admin/scontainers` korzysta z `Domain\Scontainers\ScontainersRepository` (DI kontroler + fasada legacy).
**Aktualizacja 2026-02-12 (ver. 0.260):** modul `/admin/articles_archive` korzysta z `Domain\Article\ArticleRepository` (`listArchivedForAdmin`, `restore`, `deletePermanently`) przez `admin\Controllers\ArticlesArchiveController`.
## pp_shop_attributes
Cechy produktu (modul `/admin/shop_attribute`).
| Kolumna | Opis |
|---------|------|
| id | PK |
| status | Status: 1 = aktywny, 0 = nieaktywny |
| type | Typ cechy: 0 = tekst, 1 = kolor, 2 = wzor |
| o | Kolejnosc wyswietlania |
**Uzywane w:** `Domain\Attribute\AttributeRepository`, `admin\Controllers\ShopAttributeController`, `admin\controls\ShopProduct`, `admin\factory\ShopProduct`
## pp_shop_attributes_langs
Tlumaczenia cech produktu (per jezyk).
| Kolumna | Opis |
|---------|------|
| id | PK |
| attribute_id | FK do pp_shop_attributes |
| lang_id | ID jezyka (np. pl, en) |
| name | Nazwa cechy |
**Uzywane w:** `Domain\Attribute\AttributeRepository`, `shop\ProductAttribute`
## pp_shop_attributes_values
Wartosci cech produktu.
| Kolumna | Opis |
|---------|------|
| id | PK |
| attribute_id | FK do pp_shop_attributes |
| is_default | Czy wartosc domyslna dla cechy (0/1) |
| impact_on_the_price | Wplyw na cene wariantu (NULL = brak) |
**Uzywane w:** `Domain\Attribute\AttributeRepository`, `admin\Controllers\ShopAttributeController`, `admin\factory\ShopProduct`
## pp_shop_attributes_values_langs
Tlumaczenia wartosci cech (per jezyk).
| Kolumna | Opis |
|---------|------|
| id | PK |
| value_id | FK do pp_shop_attributes_values |
| lang_id | ID jezyka (np. pl, en) |
| name | Nazwa wyswietlana |
| value | Wewnetrzna wartosc techniczna (opcjonalna) |
**Uzywane w:** `Domain\Attribute\AttributeRepository`, `shop\ProductAttribute`
## pp_shop_products_attributes
Powiazanie kombinacji produktow z wartosciami cech.
| Kolumna | Opis |
|---------|------|
| product_id | FK do pp_shop_products (kombinacja) |
| value_id | FK do pp_shop_attributes_values |
**Uzywane w:** `Domain\Attribute\AttributeRepository::refreshCombinationPricesForValue()`, `admin\controls\ShopProduct`, `admin\factory\ShopProduct`
**Aktualizacja 2026-02-14 (ver. 0.271):** modul `/admin/shop_attribute` korzysta z `Domain\Attribute\AttributeRepository` przez `admin\Controllers\ShopAttributeController`. Usunieto legacy klasy `admin\controls\ShopAttribute`, `admin\factory\ShopAttribute`, `admin\view\ShopAttribute`.
## pp_shop_coupon
Kody rabatowe sklepu (modul `/admin/shop_coupon`).
| Kolumna | Opis |
|---------|------|
| id | PK |
| name | Kod kuponu (UNIQUE) |
| status | Status: 1 = aktywny, 0 = nieaktywny |
| send | Czy kupon zostal wyslany (0/1) |
| used | Czy kupon zostal wykorzystany (0/1) |
| date_used | Data wykorzystania kuponu (NULL gdy brak) |
| used_count | Licznik uzyc kuponu |
| type | Typ kuponu (obecnie: 1 = rabat procentowy na koszyk) |
| amount | Wartosc kuponu (np. procent) |
| one_time | Czy kupon jednorazowy (0/1) |
| include_discounted_product | Czy obejmuje rowniez produkty przecenione (0/1) |
| categories | JSON z ID kategorii objetych kuponem (NULL = bez ograniczenia) |
**Uzywane w:** `Domain\Coupon\CouponRepository`, `admin\Controllers\ShopCouponController`, `shop\Coupon`, `front\factory\ShopCoupon`, `front\factory\ShopOrder`
**Aktualizacja 2026-02-13 (ver. 0.266):** modul `/admin/shop_coupon` korzysta z `Domain\Coupon\CouponRepository` przez `admin\Controllers\ShopCouponController`. Usunieto legacy klasy `admin\controls\ShopCoupon` i `admin\factory\ShopCoupon`.
## pp_shop_promotion
Promocje sklepu (modul `/admin/shop_promotion`).
| Kolumna | Opis |
|---------|------|
| id | PK |
| name | Nazwa promocji |
| status | Status: 1 = aktywna, 0 = nieaktywna |
| condition_type | Typ warunku promocji (slownik w `shop\Promotion::$condition_type`) |
| discount_type | Typ rabatu (slownik w `shop\Promotion::$discount_type`) |
| amount | Wartosc rabatu (np. procent) |
| date_from | Data startu promocji (NULL = aktywna od razu) |
| date_to | Data konca promocji (NULL = bez daty konca) |
| categories | JSON z ID kategorii grupy I |
| condition_categories | JSON z ID kategorii grupy II |
| include_coupon | Czy laczyc z kuponami rabatowymi (0/1) |
| include_product_promo | Czy uwzgledniac produkty przecenione (0/1) |
| min_product_count | Minimalna liczba produktow (dla wybranych warunkow) |
| price_cheapest_product | Cena najtanszego produktu (dla wybranych warunkow) |
**Uzywane w:** `Domain\Promotion\PromotionRepository`, `admin\Controllers\ShopPromotionController`, `shop\Promotion`, `front\factory\ShopPromotion`
**Aktualizacja 2026-02-13:** modul `/admin/shop_promotion` korzysta z `Domain\Promotion\PromotionRepository` przez `admin\Controllers\ShopPromotionController`. Usunieto legacy klasy `admin\controls\ShopPromotion` i `admin\factory\ShopPromotion`.
**Aktualizacja 2026-02-13 (ver. 0.265):** dodano obsluge `date_from` (repozytorium, formularz admin, lista admin, filtr aktywnych promocji na froncie) oraz poprawke zapisu edycji promocji po `id`.
## pp_shop_payment_methods
Metody platnosci sklepu (modul `/admin/shop_payment_method`).
| Kolumna | Opis |
|---------|------|
| id | PK |
| name | Nazwa metody platnosci |
| description | Opis metody platnosci (wyswietlany m.in. w checkout) |
| status | Status: 1 = aktywna, 0 = nieaktywna |
| apilo_payment_type_id | ID typu platnosci Apilo (NULL gdy brak mapowania) |
| sellasist_payment_type_id | DEPRECATED (integracja Sellasist usunieta w ver. 0.263) |
**Uzywane w:** `Domain\PaymentMethod\PaymentMethodRepository`, `admin\Controllers\ShopPaymentMethodController`, `front\factory\ShopPaymentMethod`, `shop\PaymentMethod`, `admin\controls\ShopTransport`, `cron.php`
**Aktualizacja 2026-02-14 (ver. 0.268):** modul `/admin/shop_payment_method` korzysta z `Domain\PaymentMethod\PaymentMethodRepository` przez `admin\Controllers\ShopPaymentMethodController`. Usunieto legacy klasy `admin\controls\ShopPaymentMethod`, `admin\factory\ShopPaymentMethod`, `admin\view\ShopPaymentMethod` oraz widok `admin/templates/shop-payment-method/view-list.php`.
## pp_shop_transports
Rodzaje transportu sklepu (modul `/admin/shop_transport`).
| Kolumna | Opis |
|---------|------|
| id | PK |
| name | Nazwa (systemowa, readonly) |
| name_visible | Nazwa widoczna dla klienta |
| description | Opis metody transportu |
| status | Status: 1 = aktywny, 0 = nieaktywny |
| cost | Koszt dostawy (PLN) |
| max_wp | Maksymalna waga paczki (NULL = bez limitu) |
| default | Domyslna forma dostawy (0/1) |
| delivery_free | Czy obsluguje darmowa dostawe (0/1) |
| apilo_carrier_account_id | ID konta przewoznika w Apilo (NULL gdy brak mapowania) |
| o | Kolejnosc wyswietlania |
**Uzywane w:** `Domain\Transport\TransportRepository`, `admin\Controllers\ShopTransportController`, `front\factory\ShopTransport`
## pp_shop_transport_payment_methods
Powiazanie metod transportu z metodami platnosci (tabela lacznikowa).
| Kolumna | Opis |
|---------|------|
| id_transport | FK do pp_shop_transports |
| id_payment_method | FK do pp_shop_payment_methods |
**Uzywane w:** `Domain\Transport\TransportRepository`, `Domain\PaymentMethod\PaymentMethodRepository::forTransport()`
**Aktualizacja 2026-02-14 (ver. 0.269):** modul `/admin/shop_transport` korzysta z `Domain\Transport\TransportRepository` przez `admin\Controllers\ShopTransportController`. Usunieto legacy klasy `admin\controls\ShopTransport`, `admin\view\ShopTransport` oraz widok `admin/templates/shop-transport/view-list.php`.
## pp_shop_apilo_settings
Ustawienia integracji Apilo (key-value).
| Kolumna | Opis |
|---------|------|
| id | PK |
| name | Klucz ustawienia (np. client-id, access-token) |
| value | Wartosc ustawienia |
**Uzywane w:** `Domain\Integrations\IntegrationsRepository`, `admin\Controllers\IntegrationsController`, `admin\factory\Integrations`
## pp_shop_shoppro_settings
Ustawienia integracji ShopPRO (key-value).
| Kolumna | Opis |
|---------|------|
| id | PK |
| name | Klucz ustawienia (np. domain, db_name) |
| value | Wartosc ustawienia |
**Uzywane w:** `Domain\Integrations\IntegrationsRepository`, `admin\Controllers\IntegrationsController`, `admin\factory\Integrations`
**Aktualizacja 2026-02-13:** modul `/admin/integrations/` korzysta z `Domain\Integrations\IntegrationsRepository` (DI kontroler + fasada legacy). Usunieto integracje Sellasist i Baselinker.
## pp_shop_statuses
Statusy zamowien sklepu (modul `/admin/shop_statuses`). Statusy sa predefiniowane - brak dodawania/usuwania, mozliwa edycja koloru i mapowania Apilo.
| Kolumna | Opis |
|---------|------|
| id | PK (zaczyna sie od 0!) |
| status | Nazwa statusu (read-only) |
| color | Kolor statusu (hex, np. #ff0000) |
| o | Kolejnosc wyswietlania |
| apilo_status_id | ID statusu w Apilo (NULL gdy brak mapowania) |
| baselinker_status_id | DEPRECATED (usuniety w ver. 0.263) |
| sellasist_status_id | DEPRECATED (usuniety w ver. 0.263) |
**Uzywane w:** `Domain\ShopStatus\ShopStatusRepository`, `admin\Controllers\ShopStatusesController`, `front\factory\ShopStatuses`, `shop\Order`, `cron.php`
**Aktualizacja 2026-02-14 (ver. 0.267):** modul `/admin/shop_statuses` korzysta z `Domain\ShopStatus\ShopStatusRepository` przez `admin\Controllers\ShopStatusesController`. Usunieto legacy klasy `admin\controls\ShopStatuses` i `admin\factory\ShopStatuses`. `front\factory\ShopStatuses` dziala jako fasada do repozytorium.
## pp_shop_product_sets
Komplety produktow (modul `/admin/shop_product_sets`).
| Kolumna | Opis |
|---------|------|
| id | PK |
| name | Nazwa kompletu |
| status | Status: 1 = aktywny, 0 = nieaktywny |
**Uzywane w:** `Domain\ProductSet\ProductSetRepository`, `admin\Controllers\ShopProductSetsController`, `shop\ProductSet`, `shop\Product`
## pp_shop_product_sets_products
Powiazanie kompletow z produktami (tabela lacznikowa).
| Kolumna | Opis |
|---------|------|
| id | PK |
| set_id | FK do pp_shop_product_sets |
| product_id | FK do pp_shop_products |
**Uzywane w:** `Domain\ProductSet\ProductSetRepository`, `shop\Product`, `front\factory\ShopProduct`, `admin\factory\ShopProduct`
**Aktualizacja 2026-02-15 (ver. 0.272):** modul `/admin/shop_product_sets` korzysta z `Domain\ProductSet\ProductSetRepository` przez `admin\Controllers\ShopProductSetsController`. Usunieto legacy klasy `admin\controls\ShopProductSets` i `admin\factory\ShopProductSet`. `shop\ProductSet` dziala jako fasada do repozytorium.
## pp_shop_producer
Producenci produktow (modul `/admin/shop_producer`).
| Kolumna | Opis |
|---------|------|
| id | PK |
| name | Nazwa producenta |
| status | Status: 1 = aktywny, 0 = nieaktywny |
| img | Sciezka do logo producenta (NULL gdy brak) |
**Uzywane w:** `Domain\Producer\ProducerRepository`, `admin\Controllers\ShopProducerController`, `shop\Producer`, `shop\Product`, `front\controls\ShopProducer`
## pp_shop_producer_lang
Tlumaczenia producentow (per jezyk). FK kaskadowe ON DELETE CASCADE.
| Kolumna | Opis |
|---------|------|
| id | PK |
| producer_id | FK do pp_shop_producer |
| lang_id | ID jezyka (np. pl, en) |
| description | Opis producenta (TEXT) |
| data | Dane producenta (TEXT, HTML) |
| meta_title | Meta title SEO (VARCHAR 255) |
**Uzywane w:** `Domain\Producer\ProducerRepository`, `shop\Producer`, `shop\Product`
**Aktualizacja 2026-02-15 (ver. 0.273):** modul `/admin/shop_producer` korzysta z `Domain\Producer\ProducerRepository` przez `admin\Controllers\ShopProducerController`. Usunieto legacy `admin\controls\ShopProducer` i `admin\factory\ShopProducer`. `shop\Producer` dziala jako fasada do repozytorium.

View File

@@ -0,0 +1,338 @@
# Struktura Projektu shopPRO
Dokumentacja struktury projektu shopPRO do szybkiego odniesienia.
## System Cache (Redis)
### Klasy odpowiedzialne za cache
#### RedisConnection
- **Plik:** `autoload/class.RedisConnection.php`
- **Opis:** Singleton zarządzający połączeniem z Redis
- **Metody:**
- `getInstance()` - pobiera instancję połączenia
- `getConnection()` - zwraca obiekt Redis
#### CacheHandler
- **Plik:** `autoload/class.CacheHandler.php`
- **Opis:** Handler do obsługi cache Redis
- **Metody:**
- `get($key)` - pobiera wartość z cache
- `set($key, $value, $ttl = 86400)` - zapisuje wartość do cache
- `exists($key)` - sprawdza czy klucz istnieje
- `delete($key)` - usuwa pojedynczy klucz
- `deletePattern($pattern)` - usuwa klucze według wzorca
#### Klasa S (pomocnicza)
- **Plik:** `autoload/class.S.php`
- **Metody cache:**
- `clear_redis_cache()` - czyści cały cache Redis (flushAll)
- `clear_product_cache(int $product_id)` - czyści cache konkretnego produktu
### Wzorce kluczy Redis
#### Produkty
```
shop\product:{product_id}:{lang_id}:{permutation_hash}
```
- Przechowuje zserializowany obiekt produktu
- TTL: 24 godziny (86400 sekund)
- Klasa: `shop\Product::getFromCache()` - `autoload/shop/class.Product.php:121`
#### Opcje ilościowe produktu
```
\shop\Product::get_product_permutation_quantity_options:{product_id}:{permutation}
```
- Przechowuje informacje o ilości i komunikatach magazynowych
- Klasa: `shop\Product::get_product_permutation_quantity_options()` - `autoload/shop/class.Product.php:549`
#### Zestawy produktów
```
\shop\Product::product_sets_when_add_to_basket:{product_id}
```
- Przechowuje produkty często kupowane razem
- Klasa: `shop\Product::product_sets_when_add_to_basket()` - `autoload/shop/class.Product.php:316`
## Integracje z systemami zewnętrznymi (CRON)
### Plik: `cron.php`
#### Apilo
- **Aktualizacja pojedynczego produktu:** synchronizacja cen i stanow
- Czestotliwosc: Co 10 minut
- **Synchronizacja cennika:** masowa aktualizacja cen z Apilo
- Czestotliwosc: Co 1 godzine
- **Synchronizacja zaleglych syncow platnosci/statusow:** kolejka retry dla chwilowej niedostepnosci Apilo (`temp/apilo-sync-queue.json`)
- Przetwarzanie: przy kazdym uruchomieniu `cron.php` (limit wsadowy)
**Uwaga:** Integracje Sellasist i Baselinker zostaly usuniete w ver. 0.263.
## Panel Administratora
### Routing
- Główny katalog: `admin/`
- Template główny: `admin/templates/site/main-layout.php`
- Kontrolery (nowe): `autoload/admin/Controllers/`
- Kontrolery legacy (fallback): `autoload/admin/controls/`
### Przycisk "Wyczyść cache"
- **Lokalizacja UI:** `admin/templates/site/main-layout.php:172`
- **JavaScript:** `admin/templates/site/main-layout.php:235-274`
- **Endpoint AJAX:** `/admin/settings/clear_cache_ajax/`
- **Kontroler:** `autoload/admin/Controllers/SettingsController.php:43-60`
- **Działanie:**
1. Pokazuje spinner "Czyszczę cache..."
2. Czyści katalogi: `temp/`, `thumbs/`
3. Wykonuje `flushAll()` na Redis
4. Pokazuje "Cache wyczyszczony!" przez 2 sekundy
5. Przywraca stan początkowy
## Struktura katalogów
```
shopPRO/
├── admin/ # Panel administratora
│ ├── templates/ # Szablony widoków
│ └── layout/ # Zasoby CSS/JS/ikony
├── autoload/ # Klasy autoloadowane
│ ├── admin/ # Klasy panelu admin
│ │ ├── Controllers/ # Nowe kontrolery DI
│ │ ├── controls/ # Kontrolery legacy (fallback)
│ │ └── factory/ # Fabryki/helpery
│ ├── Domain/ # Repozytoria/logika domenowa
│ ├── front/ # Klasy frontendu
│ │ └── factory/ # Fabryki/helpery
│ └── shop/ # Klasy sklepu
├── docs/ # Dokumentacja techniczna
├── libraries/ # Biblioteki zewnętrzne
├── temp/ # Cache tymczasowy
├── thumbs/ # Miniatury zdjęć
└── cron.php # Zadania CRON
```
## Baza danych
### Główne tabele produktów
- `pp_shop_products` - produkty główne
- `pp_shop_products_langs` - tłumaczenia produktów
- `pp_shop_products_images` - zdjęcia produktów
- `pp_shop_products_categories` - kategorie produktów
- `pp_shop_products_custom_fields` - pola własne produktów
### Tabele integracji
- Kolumny w `pp_shop_products`:
- `apilo_product_id`, `apilo_product_name`, `apilo_get_data_date`
- Tabele ustawien:
- `pp_shop_apilo_settings` (key-value)
- `pp_shop_shoppro_settings` (key-value)
### Tabele checkout
- `pp_shop_payment_methods` - metody platnosci sklepu (mapowanie `apilo_payment_type_id`)
- `pp_shop_transports` - rodzaje transportu sklepu (mapowanie `apilo_carrier_account_id`)
- `pp_shop_transport_payment_methods` - powiazanie metod transportu i platnosci
Pelna dokumentacja tabel: `DATABASE_STRUCTURE.md`
## Konfiguracja
### Redis
- Konfiguracja: `config.php` (zmienna `$config['redis']`)
- Parametry: host, port, password
### Autoload
- Funkcja: `__autoload_my_classes()` w `cron.php:6`
- Wzorzec: `autoload/{namespace}/class.{ClassName}.php`
## Klasy pomocnicze
### \S (autoload/class.S.php)
Główna klasa helper z metodami:
- `seo($val)` - generowanie URL SEO
- `normalize_decimal($val, $precision)` - normalizacja liczb
- `send_email()` - wysyłanie emaili
- `delete_dir($dir)` - usuwanie katalogów
- `htacces()` - generowanie .htaccess i sitemap.xml
### Medoo
- Plik: `libraries/medoo/medoo.php`
- Zmienna: `$mdb`
- ORM do operacji na bazie danych
## Najważniejsze wzorce
### Namespace'y
- `\admin\Controllers\` - nowe kontrolery panelu admin (DI)
- `\admin\controls\` - kontrolery legacy (fallback)
- `\Domain\` - repozytoria/logika domenowa
- `\admin\factory\` - helpery/fabryki admin
- `\front\factory\` - helpery/fabryki frontend
- `\shop\` - klasy sklepu (Product, Order, itp.)
### Cachowanie produktów
```php
// Pobranie produktu z cache
$product = \shop\Product::getFromCache($product_id, $lang_id, $permutation_hash);
// Czyszczenie cache produktu
\S::clear_product_cache($product_id);
// Czyszczenie całego cache
\S::clear_redis_cache();
```
## Refaktoryzacja do Domain-Driven Architecture
### Nowa struktura (w trakcie migracji)
```
autoload/
├── Domain/ # Nowa warstwa biznesowa (namespace \Domain\)
│ ├── Product/
│ │ └── ProductRepository.php
│ ├── Banner/
│ │ └── BannerRepository.php
│ ├── Settings/
│ │ └── SettingsRepository.php
│ ├── Cache/
│ │ └── CacheRepository.php
│ ├── Article/
│ │ └── ArticleRepository.php
│ ├── User/
│ │ └── UserRepository.php
│ ├── Languages/
│ │ └── LanguagesRepository.php
│ ├── Layouts/
│ │ └── LayoutsRepository.php
│ ├── Newsletter/
│ │ └── NewsletterRepository.php
│ ├── Scontainers/
│ │ └── ScontainersRepository.php
│ ├── Dictionaries/
│ │ └── DictionariesRepository.php
│ ├── Pages/
│ │ └── PagesRepository.php
│ ├── Integrations/
│ │ └── IntegrationsRepository.php
│ ├── Promotion/
│ │ └── PromotionRepository.php
│ ├── Coupon/
│ │ └── CouponRepository.php
│ ├── ShopStatus/
│ │ └── ShopStatusRepository.php
│ ├── Transport/
│ │ └── TransportRepository.php
│ ├── ProductSet/
│ │ └── ProductSetRepository.php
│ ├── Producer/
│ │ └── ProducerRepository.php
│ └── ...
├── admin/
│ ├── Controllers/ # Nowe kontrolery (namespace \admin\Controllers\)
│ ├── class.Site.php # Router: nowy kontroler → fallback stary
│ ├── controls/ # Stare kontrolery (niezależny fallback)
│ ├── factory/ # Stare helpery (niezależny fallback)
│ └── view/ # Widoki (statyczne - bez zmian)
├── shop/ # Legacy - fasady do Domain
└── front/factory/ # Legacy - stopniowo migrowane
```
**Aktualizacja 2026-02-14 (ver. 0.268):**
- Dodano modul domenowy `Domain/PaymentMethod/PaymentMethodRepository.php`.
- Dodano kontroler DI `admin/Controllers/ShopPaymentMethodController.php`.
- Modul `/admin/shop_payment_method/*` dziala na nowych widokach (`payment-methods-list`, `payment-method-edit`).
- Usunieto legacy: `autoload/admin/controls/class.ShopPaymentMethod.php`, `autoload/admin/factory/class.ShopPaymentMethod.php`, `autoload/admin/view/class.ShopPaymentMethod.php`, `admin/templates/shop-payment-method/view-list.php`.
**Aktualizacja 2026-02-14 (ver. 0.269):**
- Dodano modul domenowy `Domain/Transport/TransportRepository.php`.
- Dodano kontroler DI `admin/Controllers/ShopTransportController.php`.
- Modul `/admin/shop_transport/*` dziala na nowych widokach (`transports-list`, `transport-edit`).
- Usunieto legacy: `autoload/admin/controls/class.ShopTransport.php`, `autoload/admin/view/class.ShopTransport.php`, `admin/templates/shop-transport/view-list.php`.
- `admin\factory\ShopTransport` i `front\factory\ShopTransport` przepiete na repozytorium.
**Aktualizacja 2026-02-14 (ver. 0.270):**
- `shop\Order` zapisuje nieudane syncy Apilo (status/platnosc) do kolejki `temp/apilo-sync-queue.json`.
- `cron.php` automatycznie ponawia zalegle syncy (`Order::process_apilo_sync_queue()`).
- `shop\Order::set_as_paid()` wysyla mapowany typ platnosci Apilo (z mapowania metody platnosci), bez stalej wartosci `type`.
### Routing admin (admin\Site::route())
1. Sprawdź mapę `$newControllers` → utwórz instancję z DI → wywołaj
2. Jeśli nowy kontroler nie istnieje (`class_exists()` = false) → fallback na `admin\controls\`
3. Stary kontroler jest NIEZALEŻNY od nowych klas (bezpieczny fallback)
### Dependency Injection
Nowe klasy używają **Dependency Injection** zamiast `global` variables:
```php
// STARE
global $mdb;
$quantity = $mdb->get('pp_shop_products', 'quantity', ['id' => $id]);
// NOWE
$repository = new \Domain\Product\ProductRepository($mdb);
$quantity = $repository->getQuantity($id);
```
## Testowanie (tylko dla deweloperów)
**UWAGA:** Pliki testów NIE są częścią aktualizacji dla klientów!
### Narzędzia
- **PHPUnit 9.6.34** - framework testowy
- **test.bat** - uruchamianie testów
- **composer.json** - autoloading PSR-4
Pelna dokumentacja testow: `TESTING.md`
## Dodatkowa aktualizacja 2026-02-14 (ver. 0.271)
- Dodano modul domenowy `Domain/Attribute/AttributeRepository.php`.
- Dodano kontroler DI `admin/Controllers/ShopAttributeController.php`.
- Modul `/admin/shop_attribute/*` zostal przepiety na nowe widoki (`attributes-list`, `attribute-edit`, `values-edit`).
- Usunieto legacy: `autoload/admin/controls/class.ShopAttribute.php`, `autoload/admin/factory/class.ShopAttribute.php`, `autoload/admin/view/class.ShopAttribute.php`, `admin/templates/shop-attribute/_partials/value.php`.
- Przepieto zaleznosci kombinacji produktu na `Domain\Attribute\AttributeRepository` i `shop\ProductAttribute`.
- Dla `ShopAttribute` routing celowo nie wykonuje fallbacku akcji do legacy kontrolera.
## Dodatkowa aktualizacja 2026-02-15 (ver. 0.272)
- Dodano modul domenowy `Domain/ProductSet/ProductSetRepository.php`.
- Dodano kontroler DI `admin/Controllers/ShopProductSetsController.php`.
- Modul `/admin/shop_product_sets/*` dziala na nowych widokach (`product-sets-list`, `product-set-edit`).
- Usunieto legacy: `autoload/admin/controls/class.ShopProductSets.php`, `autoload/admin/factory/class.ShopProductSet.php`, `admin/templates/shop-product-sets/view-list.php`, `admin/templates/shop-product-sets/set-edit.php`.
- `shop\ProductSet` przepiety na fasade do `Domain\ProductSet\ProductSetRepository`.
## Dodatkowa aktualizacja 2026-02-15 (ver. 0.273)
- Dodano modul domenowy `Domain/Producer/ProducerRepository.php`.
## Dodatkowa aktualizacja 2026-02-15 (ver. 0.274)
- Dodano modul domenowy `Domain/Client/ClientRepository.php`.
- Dodano kontroler DI `admin/Controllers/ShopClientsController.php`.
- Modul `/admin/shop_clients/*` dziala na nowych widokach opartych o `components/table-list`.
- Usunieto legacy: `autoload/admin/controls/class.ShopClients.php`, `autoload/admin/factory/class.ShopClients.php`.
- Routing i menu admin przepiete na kanoniczny URL `/admin/shop_clients/list/`.
- Dodano kontroler DI `admin/Controllers/ShopProducerController.php`.
- Modul `/admin/shop_producer/*` dziala na nowych widokach (`producers-list`, `producer-edit`).
- Usunieto legacy: `autoload/admin/controls/class.ShopProducer.php`, `admin/templates/shop-producer/list.php`, `admin/templates/shop-producer/edit.php`.
- `shop\Producer` przepiety na fasade do `Domain\Producer\ProducerRepository`.
- `admin\controls\ShopProduct` uzywa `ProducerRepository::allProducers()`.
- Usunieto 6 pustych factory facades: `admin\factory\Languages`, `admin\factory\Newsletter`, `admin\factory\Scontainers`, `admin\factory\ShopProducer`, `admin\factory\ShopTransport`, `admin\factory\Layouts`.
- Przepieto 2 wywolania `admin\factory\ShopTransport` w `admin\factory\ShopProduct` na `Domain\Transport\TransportRepository`.
- Usuniety fallback do `admin\factory\Layouts` w `admin\controls\ShopProduct`.
## Dodatkowa aktualizacja 2026-02-15 (ver. 0.274)
- Dodano kontroler DI `admin/Controllers/ShopProductController.php` (akcje `mass_edit`, `mass_edit_save`, `get_products_by_category`).
- Routing `admin\Site` rozszerzono o mapowanie `ShopProduct` do nowego kontrolera.
- `Domain/Product/ProductRepository.php` rozszerzono o metody dla mass-edit: `allProductsForMassEdit`, `getProductsByCategory`, `applyDiscountPercent`.
- Usunieto legacy akcje mass-edit z `autoload/admin/controls/class.ShopProduct.php`.
- Widok `/admin/shop_product/mass_edit/` przepiety na nowy partial `admin/templates/shop-product/mass-edit-custom-script.php`.
- Ujednolicono UI drzewek (strzalki/expand) w:
- `admin/templates/pages/pages-list.php` + `admin/templates/pages/subpages-list.php`
- `admin/templates/articles/subpages-list.php` + `admin/templates/articles/article-edit-custom-script.php`
## Dodatkowa aktualizacja 2026-02-15 (ver. 0.275)
- Dodano modul domenowy `Domain/Category/CategoryRepository.php`.
- Dodano kontroler DI `admin/Controllers/ShopCategoryController.php`.
- Modul `/admin/shop_category/*` dziala przez DI i kanoniczny URL `/admin/shop_category/list/` (z zachowaniem aliasu `view_list`).
- Widoki `shop-category/*` maja wydzielone skrypty `*-custom-script.php` i ujednolicone strzalki drzewa (`button + caret + aria-expanded`).
- Endpointy AJAX dla drzewka kategorii i kolejnosci produktow przepiete na `/admin/shop_category/save_categories_order/`, `/admin/shop_category/save_products_order/`, `/admin/shop_category/cookie_categories/`.
- Usunieto legacy: `autoload/admin/controls/class.ShopCategory.php`, `autoload/admin/factory/class.ShopCategory.php`, `autoload/admin/view/class.ShopCategory.php`.
- Przepieto zaleznosci `ShopProduct` z `admin\factory\ShopCategory` na `Domain\Category\CategoryRepository`.
- Usunieto preload `autoload/admin/factory/class.ShopCategory.php` z `libraries/grid/config.php`.
---
*Dokument aktualizowany: 2026-02-15*

View File

@@ -0,0 +1,297 @@
# Plan Refaktoryzacji shopPRO - Domain-Driven Architecture
## Cel
Stopniowe przeniesienie logiki biznesowej do architektury warstwowej:
- **Domain/** - logika biznesowa (core)
- **Admin/** - warstwa administratora
- **Frontend/** - warstwa użytkownika
- **Shared/** - współdzielone narzędzia
## Docelowa struktura
```
autoload/
├── Domain/ # Logika biznesowa (CORE) - namespace \Domain\
│ ├── Product/
│ │ ├── ProductRepository.php
│ │ ├── ProductService.php # (przyszłość)
│ │ └── ProductCacheService.php # (przyszłość)
│ ├── Banner/
│ │ └── BannerRepository.php
│ ├── Settings/
│ │ └── SettingsRepository.php
│ ├── Cache/
│ │ └── CacheRepository.php
│ ├── Order/
│ ├── Category/
│ └── ...
├── admin/ # Warstwa administratora (istniejący katalog!)
│ ├── Controllers/ # Nowe kontrolery - namespace \admin\Controllers\
│ ├── controls/ # Stare kontrolery (legacy fallback)
│ ├── factory/ # Stare helpery (legacy)
│ └── view/ # Widoki (statyczne - OK bez zmian)
├── Frontend/ # Warstwa użytkownika (przyszłość)
│ ├── Controllers/
│ └── Services/
├── Shared/ # Współdzielone narzędzia
│ ├── Cache/
│ │ ├── CacheHandler.php
│ │ └── RedisConnection.php
│ └── Helpers/
│ └── S.php
└── [LEGACY] # Stare klasy (stopniowo deprecated)
├── shop/
├── admin/factory/
└── front/factory/
```
### WAŻNE: Konwencja namespace → katalog (Linux case-sensitive!)
- `\Domain\``autoload/Domain/` (duże D - nowy katalog)
- `\admin\Controllers\``autoload/admin/Controllers/` (małe a - istniejący katalog)
- NIE używać `\Admin\` (duże A) bo na serwerze Linux katalog to `admin/` (małe a)
## Zasady migracji
### 1. Stopniowość
- Przenosimy **jedną funkcję na raz**
- Zachowujemy kompatybilność wsteczną
- Stare klasy działają jako fasady do nowych
### 2. Dependency Injection zamiast statycznych metod
```php
// ❌ STARE - statyczne
class Product {
public static function getQuantity($id) {
global $mdb;
return $mdb->get('pp_shop_products', 'quantity', ['id' => $id]);
}
}
// ✅ NOWE - instancje z DI
class ProductRepository {
private $db;
public function __construct($db) {
$this->db = $db;
}
public function getQuantity($id) {
return $this->db->get('pp_shop_products', 'quantity', ['id' => $id]);
}
}
```
### 3. Fasady dla kompatybilności
```php
// Stara klasa wywołuje nową
namespace shop;
class Product {
public static function getQuantity($id) {
global $mdb;
$repo = new \Domain\Product\ProductRepository($mdb);
return $repo->getQuantity($id);
}
}
```
## Proces migracji funkcji
### Krok 1: Wybór funkcji
- Wybierz prostą funkcję statyczną
- Sprawdź jej zależności
- Przeanalizuj gdzie jest używana
### Krok 2: Stworzenie nowej struktury
- Utwórz folder `Domain/{Module}/`
- Stwórz odpowiednią klasę (Repository/Service/Entity)
- Przenieś logikę
### Krok 3: Znalezienie użyć
```bash
grep -r "Product::getQuantity" .
```
### Krok 4: Aktualizacja wywołań
- Opcja A: Bezpośrednie wywołanie nowej klasy
- Opcja B: Fasada w starej klasie (zalecane na początek)
### Krok 5: Testy
- Napisz test jednostkowy dla nowej funkcji
- Sprawdź czy stare wywołania działają
## Status migracji
### ✅ Zmigrowane moduły
| # | Modul | Wersja | Zakres |
|---|-------|--------|--------|
| 1 | Cache | 0.237 | CacheHandler, RedisConnection, clear_product_cache |
| 2 | Product | 0.238-0.252, 0.274 | getQuantity, getPrice, getName, archive/unarchive, allProductsForMassEdit, getProductsByCategory, applyDiscountPercent |
| 3 | Banner | 0.239 | find, delete, save, kontroler DI |
| 4 | Settings | 0.240/0.250 | saveSettings, getSettings, kontroler DI |
| 5 | Dictionaries | 0.251 | listForAdmin, find, save, delete, kontroler DI |
| 6 | ProductArchive | 0.252 | kontroler DI, table-list |
| 7 | Filemanager | 0.252 | kontroler DI, fix Invalid Key |
| 8 | Users | 0.253 | CRUD, logon, 2FA, kontroler DI |
| 9 | Languages | 0.254 | languages + translations, kontroler DI |
| 10 | Layouts | 0.256 | find, save, delete, menusWithPages, categoriesTree |
| 11 | Newsletter | 0.257-0.258 | subskrybenci, szablony, ustawienia |
| 12 | Scontainers | 0.259 | listForAdmin, find, save, delete |
| 13 | ArticlesArchive | 0.260 | restore, deletePermanently |
| 14 | Articles | 0.261 | pelna migracja (CRUD, AJAX, galeria, pliki) |
| 15 | Pages | 0.262 | menu/page CRUD, drzewo stron, AJAX |
| 16 | Integrations | 0.263 | Apilo/ShopPRO, cleanup Sellasist/Baselinker |
| 17 | ShopPromotion | 0.264-0.265 | listForAdmin, find, save, delete, categoriesTree |
| 18 | ShopCoupon | 0.266 | listForAdmin, find, save, delete, categoriesTree |
| 19 | ShopStatuses | 0.267 | listForAdmin, find, save, color picker |
| 20 | ShopPaymentMethod | 0.268 | listForAdmin, find, save, allActive, mapowanie Apilo, DI kontroler |
| 21 | ShopTransport | 0.269 | listForAdmin, find, save, allActive, allForAdmin, findActiveById, getTransportCost, lowestTransportPrice, getApiloCarrierAccountId, powiazanie z PaymentMethod, DI kontroler |
| 22 | ShopAttribute | 0.271 | list/edit/save/delete/values, nowy edytor wartosci, cleanup legacy, przepiecie zaleznosci kombinacji |
| 23 | ShopProductSets | 0.272 | listForAdmin, find, save, delete, allSets, allProductsMap, multi-select Selectize, DI kontroler |
| 24 | ShopProducer | 0.273 | listForAdmin, find, save, delete, allProducers, producerProducts, fasada shop\Producer, DI kontroler |
| 25 | ShopProduct (mass_edit) | 0.274 | DI kontroler + routing dla `mass_edit`, `mass_edit_save`, `get_products_by_category`, cleanup legacy akcji |
| 26 | ShopClients | 0.274 | DI kontroler + routing dla `list/details`, nowe listy na `components/table-list`, cleanup legacy controls/factory |
| 27 | ShopCategory | 0.275 | CategoryRepository + DI kontroler + routing, endpointy AJAX (`save_categories_order`, `save_products_order`, `cookie_categories`), cleanup legacy controls/factory/view |
### Product - szczegolowy status
- ✅ getQuantity (ver. 0.238)
- ✅ getPrice (ver. 0.239)
- ✅ getName (ver. 0.239)
- ✅ archive / unarchive (ver. 0.241/0.252)
- ✅ allProductsForMassEdit (ver. 0.274)
- ✅ getProductsByCategory (ver. 0.274)
- ✅ applyDiscountPercent (ver. 0.274)
- [ ] is_product_on_promotion
- [ ] getFromCache
- [ ] getProductImg
### 📋 Do zrobienia
- Order
- ShopProduct (factory)
## Kolejność refaktoryzacji (priorytet)
1-27: ✅ Cache, Product, Banner, Settings, Dictionaries, ProductArchive, Filemanager, Users, Pages, Integrations, ShopPromotion, ShopCoupon, ShopStatuses, ShopPaymentMethod, ShopTransport, ShopAttribute, ShopProductSets, ShopProducer, ShopProduct (mass_edit), ShopClients, ShopCategory
Nastepne:
28. **Order**
## Form Edit System
Nowy uniwersalny system formularzy edycji:
- ✅ Klasy ViewModel: `FormFieldType`, `FormField`, `FormTab`, `FormAction`, `FormEditViewModel`
- ✅ Walidacja: `FormValidator` z obsługą reguł per pole i sekcje językowe
- ✅ Persist: `FormRequestHandler` - zapamiętywanie danych przy błędzie walidacji
- ✅ Renderer: `FormFieldRenderer` - renderowanie wszystkich typów pól
- ✅ Szablon: `admin/templates/components/form-edit.php` - uniwersalny layout
- Wspierane typy pól: text, number, email, password, date, datetime, switch, select, textarea, editor, image, file, hidden, lang_section, color
- Obsługa zakładek (vertical) i sekcji językowych (horizontal)
- **Do zrobienia**: Przerobić pozostałe kontrolery/formularze (Product, Category, Pages, itd.)
Pelna dokumentacja: `docs/FORM_EDIT_SYSTEM.md`
## Zasady kodu
### 1. SOLID Principles
- **S**ingle Responsibility - jedna klasa = jedna odpowiedzialność
- **O**pen/Closed - otwarty na rozszerzenia, zamknięty na modyfikacje
- **L**iskov Substitution - podklasy mogą zastąpić nadklasy
- **I**nterface Segregation - wiele małych interfejsów
- **D**ependency Inversion - zależności od abstrakcji
### 2. Nazewnictwo
- **Entity** - `Product.php` (reprezentuje obiekt domenowy)
- **Repository** - `ProductRepository.php` (dostęp do danych)
- **Service** - `ProductService.php` (logika biznesowa)
- **Controller** - `ProductController.php` (obsługa requestów)
### 3. Type Hinting
```php
// ✅ DOBRE
public function getQuantity(int $id): ?int {
return $this->db->get('pp_shop_products', 'quantity', ['id' => $id]);
}
// ❌ ZŁE
public function getQuantity($id) {
return $this->db->get('pp_shop_products', 'quantity', ['id' => $id]);
}
```
## Narzędzia pomocnicze
### Autoloader (produkcja)
Autoloader w 9 entry pointach obsługuje dwie konwencje:
1. `autoload/{namespace}/class.{ClassName}.php` (legacy)
2. `autoload/{namespace}/{ClassName}.php` (PSR-4, fallback)
Entry pointy: `index.php`, `ajax.php`, `api.php`, `cron.php`, `cron-turstmate.php`, `download.php`, `admin/index.php`, `admin/ajax.php`, `cron/cron-xml.php`
### Static Analysis
```bash
composer require --dev phpstan/phpstan
vendor/bin/phpstan analyse autoload/Domain
```
## Testowanie
### Framework: PHPUnit
```bash
composer test
```
### Struktura testów
```
tests/
├── Unit/
│ ├── Domain/
│ │ ├── Article/ArticleRepositoryTest.php
│ │ ├── Banner/BannerRepositoryTest.php
│ │ ├── Cache/CacheRepositoryTest.php
│ │ ├── Coupon/CouponRepositoryTest.php
│ │ ├── Dictionaries/DictionariesRepositoryTest.php
│ │ ├── Integrations/IntegrationsRepositoryTest.php
│ │ ├── PaymentMethod/PaymentMethodRepositoryTest.php
│ │ ├── Producer/ProducerRepositoryTest.php
│ │ ├── Product/ProductRepositoryTest.php
│ │ ├── ProductSet/ProductSetRepositoryTest.php
│ │ ├── Promotion/PromotionRepositoryTest.php
│ │ ├── Settings/SettingsRepositoryTest.php
│ │ ├── ShopStatus/ShopStatusRepositoryTest.php
│ │ └── User/UserRepositoryTest.php
│ └── admin/
│ └── Controllers/
│ ├── ArticlesControllerTest.php
│ ├── DictionariesControllerTest.php
│ ├── IntegrationsControllerTest.php
│ ├── ProductArchiveControllerTest.php
│ ├── SettingsControllerTest.php
│ ├── ShopCouponControllerTest.php
│ ├── ShopPaymentMethodControllerTest.php
│ ├── ShopProducerControllerTest.php
│ ├── ShopProductSetsControllerTest.php
│ ├── ShopPromotionControllerTest.php
│ ├── ShopStatusesControllerTest.php
│ └── UsersControllerTest.php
└── Integration/
```
**Lacznie: 338 testow, 1063 asercji**
Aktualizacja 2026-02-15 (ver. 0.273):
- dodano testy `tests/Unit/Domain/Producer/ProducerRepositoryTest.php`
- dodano testy `tests/Unit/admin/Controllers/ShopProducerControllerTest.php`
Aktualizacja 2026-02-14 (ver. 0.271):
- dodano testy `tests/Unit/Domain/Attribute/AttributeRepositoryTest.php`
- dodano testy `tests/Unit/admin/Controllers/ShopAttributeControllerTest.php`
Pelna dokumentacja testow: `TESTING.md`
---
*Rozpoczęto: 2025-02-05*
*Ostatnia aktualizacja: 2026-02-15*
*Changelog zmian: `docs/CHANGELOG.md`*

View File

@@ -0,0 +1,456 @@
# Testowanie shopPRO
## Szybki start
### Pelny zestaw testow
```bash
composer test
```
Alternatywnie (Windows):
```bash
./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
```
### Konkretny test (`--filter`)
```bash
./test.ps1 --filter testGetQuantityReturnsCorrectValue
```
## Aktualny stan suite
Ostatnio zweryfikowano: 2026-02-15
```text
OK (351 tests, 1091 assertions)
```
Aktualizacja po migracji ShopClients (2026-02-15, ver. 0.274) - testy punktowe:
```text
OK (10 tests, 34 assertions)
```
Aktualizacja po migracji ShopCategory (2026-02-15, ver. 0.275) - testy punktowe:
```text
OK (16 tests, 72 assertions)
```
Nowe testy dodane 2026-02-15:
- `tests/Unit/Domain/Client/ClientRepositoryTest.php`
- `tests/Unit/admin/Controllers/ShopClientsControllerTest.php`
- `tests/Unit/Domain/Category/CategoryRepositoryTest.php`
- `tests/Unit/admin/Controllers/ShopCategoryControllerTest.php`
## Struktura testow
```text
tests/
|-- bootstrap.php
|-- Unit/
| |-- Domain/
| | |-- Article/ArticleRepositoryTest.php
| | |-- Attribute/AttributeRepositoryTest.php
| | |-- Banner/BannerRepositoryTest.php
| | |-- Cache/CacheRepositoryTest.php
| | |-- Coupon/CouponRepositoryTest.php
| | |-- Category/CategoryRepositoryTest.php
| | |-- Dictionaries/DictionariesRepositoryTest.php
| | |-- Integrations/IntegrationsRepositoryTest.php
| | |-- PaymentMethod/PaymentMethodRepositoryTest.php
| | |-- Producer/ProducerRepositoryTest.php
| | |-- Product/ProductRepositoryTest.php
| | |-- ProductSet/ProductSetRepositoryTest.php
| | |-- Promotion/PromotionRepositoryTest.php
| | |-- Settings/SettingsRepositoryTest.php
| | |-- ShopStatus/ShopStatusRepositoryTest.php
| | |-- Transport/TransportRepositoryTest.php
| | `-- User/UserRepositoryTest.php
| `-- admin/
| `-- Controllers/
| |-- ArticlesControllerTest.php
| |-- DictionariesControllerTest.php
| |-- IntegrationsControllerTest.php
| |-- ProductArchiveControllerTest.php
| |-- SettingsControllerTest.php
| |-- ShopAttributeControllerTest.php
| |-- ShopCategoryControllerTest.php
| |-- ShopCouponControllerTest.php
| |-- ShopPaymentMethodControllerTest.php
| |-- ShopProducerControllerTest.php
| |-- ShopProductControllerTest.php
| |-- ShopProductSetsControllerTest.php
| |-- ShopPromotionControllerTest.php
| |-- ShopStatusesControllerTest.php
| |-- ShopTransportControllerTest.php
| `-- UsersControllerTest.php
`-- Integration/
```
## Tryby uruchamiania
### 1. TestDox (czytelna lista)
```bash
./test.bat
```
Uruchamia:
```bash
C:\xampp\php\php.exe phpunit.phar --testdox
```
### 2. Standard (kropki)
```bash
./test-simple.bat
```
Uruchamia:
```bash
C:\xampp\php\php.exe phpunit.phar
```
### 3. Debug (pelne logowanie)
```bash
./test-debug.bat
```
Uruchamia:
```bash
C:\xampp\php\php.exe phpunit.phar --debug
```
### 4. PowerShell (najbardziej niezawodne)
```bash
./test.ps1
```
- najpierw probuje `php` z PATH
- jesli brak, probuje m.in. `C:\xampp\php\php.exe`
- zawsze dodaje `--do-not-cache-result`
## Interpretacja wynikow
```text
. = test przeszedl
E = error (blad wykonania)
F = failure (niezgodna asercja)
```
Przyklad sukcesu:
```text
................................................................. 65 / 82 ( 79%)
................. 82 / 82 (100%)
OK (82 tests, 181 assertions)
```
## Dodawanie nowych testow
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.
## Mockowanie (przyklad)
```php
$mockDb = $this->createMock(\medoo::class);
$mockDb->method('get')->willReturn(42);
$repo = new ProductRepository($mockDb);
$value = $repo->getQuantity(123);
$this->assertEquals(42, $value);
```
## Przydatne informacje
- Konfiguracja PHPUnit: `phpunit.xml`
- Bootstrap testow: `tests/bootstrap.php`
- Dodatkowy opis: `tests/README.md`
## Aktualizacja suite
Ostatnio zweryfikowano: 2026-02-12
```text
OK (119 tests, 256 assertions)
```
Nowe testy dodane 2026-02-12:
- `tests/Unit/Domain/User/UserRepositoryTest.php` (25 testow: CRUD, logon, 2FA verify/send, checkLogin, updateById)
- `tests/Unit/admin/Controllers/UsersControllerTest.php` (12 testow: kontrakty + normalizeUser)
Aktualizacja po migracji widokow Users (2026-02-12):
```text
OK (120 tests, 262 assertions)
```
## Aktualizacja suite (finalizacja Users)
Ostatnio zweryfikowano: 2026-02-12
```text
OK (120 tests, 262 assertions)
```
Aktualizacja po migracji Languages (2026-02-12):
```text
OK (130 tests, 301 assertions)
```
Nowe testy dodane 2026-02-12:
- `tests/Unit/Domain/Languages/LanguagesRepositoryTest.php`
- `tests/Unit/admin/Controllers/LanguagesControllerTest.php`
## Aktualizacja suite (release 0.254)
Ostatnio zweryfikowano: 2026-02-12
```text
OK (130 tests, 301 assertions)
```
Nowe testy dodane 2026-02-12:
- `tests/Unit/Domain/Languages/LanguagesRepositoryTest.php`
- `tests/Unit/admin/Controllers/LanguagesControllerTest.php`
## Aktualizacja suite (release 0.255)
Ostatnio zweryfikowano: 2026-02-12
```text
OK (130 tests, 303 assertions)
```
## Aktualizacja suite (release 0.256)
Ostatnio zweryfikowano: 2026-02-12
```text
OK (141 tests, 336 assertions)
```
Nowe testy dodane 2026-02-12:
- `tests/Unit/Domain/Layouts/LayoutsRepositoryTest.php`
- `tests/Unit/admin/Controllers/LayoutsControllerTest.php`
Zaktualizowane testy 2026-02-12:
- `tests/Unit/Domain/Languages/LanguagesRepositoryTest.php` (defaultLanguageId)
- `tests/Unit/admin/Controllers/ArticlesControllerTest.php` (konstruktor + LayoutsRepository)
## Aktualizacja suite (release 0.257)
Ostatnio zweryfikowano: 2026-02-12
```text
OK (150 tests, 372 assertions)
```
Nowe testy dodane 2026-02-12:
- `tests/Unit/Domain/Newsletter/NewsletterRepositoryTest.php`
- `tests/Unit/admin/Controllers/NewsletterControllerTest.php`
## Aktualizacja suite (release 0.258)
Ostatnio zweryfikowano: 2026-02-12
```text
OK (150 tests, 372 assertions)
```
## Aktualizacja suite (release 0.259)
Ostatnio zweryfikowano: 2026-02-12
```text
OK (158 tests, 397 assertions)
```
Nowe testy dodane 2026-02-12:
- `tests/Unit/Domain/Scontainers/ScontainersRepositoryTest.php`
- `tests/Unit/admin/Controllers/ScontainersControllerTest.php`
## Aktualizacja suite (release 0.260)
Ostatnio zweryfikowano: 2026-02-12
```text
OK (165 tests, 424 assertions)
```
Nowe testy dodane 2026-02-12:
- `tests/Unit/Domain/Article/ArticleRepositoryTest.php` (rozszerzenie o testy `restore`, `deletePermanently`, `listArchivedForAdmin`)
- `tests/Unit/admin/Controllers/ArticlesArchiveControllerTest.php`
## Aktualizacja suite (release 0.261)
Ostatnio zweryfikowano: 2026-02-13
```text
OK (176 tests, 439 assertions)
```
Nowe testy/rozszerzenia 2026-02-13:
- `tests/Unit/Domain/Article/ArticleRepositoryTest.php` (nowe przypadki dla `pagesSummaryForArticles`, `updateImageAlt`, `markFileToDelete`)
- `tests/Unit/admin/Controllers/ArticlesControllerTest.php` (nowe kontrakty dla akcji `imageAltChange`, `fileNameChange`, `imageDelete`, `fileDelete`)
## Aktualizacja suite (release 0.261)
Ostatnio zweryfikowano: 2026-02-13
```text
OK (178 tests, 443 assertions)
```
Nowe testy/rozszerzenia 2026-02-13:
- `tests/Unit/Domain/Article/ArticleRepositoryTest.php` (nowe przypadki dla `saveFilesOrder`)
## Aktualizacja suite (Pages migration)
Ostatnio zweryfikowano: 2026-02-13
```text
OK (186 tests, 478 assertions)
```
Nowe testy dodane 2026-02-13:
- `tests/Unit/Domain/Pages/PagesRepositoryTest.php`
- `tests/Unit/admin/Controllers/PagesControllerTest.php`
Zaktualizowane testy 2026-02-13:
- `tests/Unit/admin/Controllers/ArticlesControllerTest.php` (konstruktor z `Domain\\Pages\\PagesRepository`)
## Aktualizacja suite (Integrations refactor, ver. 0.263)
Ostatnio zweryfikowano: 2026-02-13
```text
OK (212 tests, 577 assertions)
```
Nowe testy dodane 2026-02-13:
- `tests/Unit/Domain/Integrations/IntegrationsRepositoryTest.php` (16 testow: getSettings, getSetting, saveSetting, linkProduct, unlinkProduct, getProductSku, apiloGetAccessToken, invalid provider, settings table mapping)
- `tests/Unit/admin/Controllers/IntegrationsControllerTest.php` (10 testow: kontrakty metod, return types, brak metod sellasist/baselinker)
Zaktualizowane pliki:
- `tests/bootstrap.php` (dodany stub `S::remove_special_chars()`)
## Aktualizacja suite (ShopPromotion refactor, ver. 0.264)
Ostatnio zweryfikowano: 2026-02-13
```text
OK (222 tests, 609 assertions)
```
Nowe testy dodane 2026-02-13:
- `tests/Unit/Domain/Promotion/PromotionRepositoryTest.php` (6 testow: find default, save insert, delete, whitelist sortowania, drzewo kategorii)
- `tests/Unit/admin/Controllers/ShopPromotionControllerTest.php` (4 testy: kontrakty metod i DI konstruktora)
## Aktualizacja suite (ShopPromotion fix + date_from, ver. 0.265)
Ostatnio zweryfikowano: 2026-02-13
```text
OK (222 tests, 614 assertions)
```
Zmiany testowe 2026-02-13:
- rozszerzenie `tests/Unit/Domain/Promotion/PromotionRepositoryTest.php` o asercje `date_from`
## Aktualizacja suite (ShopCoupon refactor, ver. 0.266)
Ostatnio zweryfikowano: 2026-02-13
```text
OK (235 tests, 682 assertions)
```
Nowe testy dodane 2026-02-13:
- `tests/Unit/Domain/Coupon/CouponRepositoryTest.php` (8 testow: find default/normalize, save insert/update, delete, whitelist sortowania, drzewo kategorii)
- `tests/Unit/admin/Controllers/ShopCouponControllerTest.php` (5 testow: kontrakty metod, aliasy legacy, DI konstruktora)
Ponowna weryfikacja po poprawkach UI (drzewko + checkboxy): 2026-02-13
- `OK (235 tests, 682 assertions)`
## Aktualizacja suite (ShopStatuses refactor, ver. 0.267)
Ostatnio zweryfikowano: 2026-02-14
```text
OK (254 tests, 736 assertions)
```
Nowe testy dodane 2026-02-14:
- `tests/Unit/Domain/ShopStatus/ShopStatusRepositoryTest.php` (9 testow: find z ID=0, find null apilo, save update, save z ID=0, empty apilo sets null, reject negative ID, getApiloStatusId, getByIntegrationStatusId, allStatuses, whitelist sortowania)
- `tests/Unit/admin/Controllers/ShopStatusesControllerTest.php` (5 testow: kontrakty metod, brak aliasow legacy, return types, DI konstruktora)
## Aktualizacja suite (ShopPaymentMethod refactor, ver. 0.268)
Ostatnio zweryfikowano: 2026-02-14
```text
OK (280 tests, 828 assertions)
```
Nowe testy dodane 2026-02-14:
- `tests/Unit/Domain/PaymentMethod/PaymentMethodRepositoryTest.php` (14 testow: find invalid/null/normalize, save update/null/non-numeric apilo, listForAdmin whitelist, allActive, allForAdmin, findActiveById, isActive, getApiloPaymentTypeId, forTransport)
- `tests/Unit/admin/Controllers/ShopPaymentMethodControllerTest.php` (5 testow: kontrakty metod, brak aliasow legacy, return types, DI konstruktora)
## Aktualizacja suite (ShopTransport refactor, ver. 0.269)
Ostatnio zweryfikowano: 2026-02-14
```text
OK (300 tests, 895 assertions)
```
Nowe testy dodane 2026-02-14:
- `tests/Unit/Domain/Transport/TransportRepositoryTest.php` (14 testow: find invalid/null/normalize/nullables, save insert/update/failure/default reset/switch normalization, listForAdmin whitelist, allActive, getApiloCarrierAccountId, getTransportCost, allForAdmin)
- `tests/Unit/admin/Controllers/ShopTransportControllerTest.php` (5 testow: kontrakty metod, brak aliasow legacy, return types, DI konstruktora z 2 repo)
## Aktualizacja suite (Apilo sync hardening, ver. 0.270)
Ostatnio zweryfikowano: 2026-02-14
```text
OK (300 tests, 895 assertions)
```
Zmiany testowe 2026-02-14:
- brak nowych testow; pelna regresja po zmianach sync Apilo (TPAY -> Apilo) przeszla bez bledow
## Aktualizacja suite (ShopAttribute refactor, ver. 0.271)
Ostatnio zweryfikowano: 2026-02-14
```text
OK (312 tests, 948 assertions)
```
Nowe testy dodane 2026-02-14:
- `tests/Unit/Domain/Attribute/AttributeRepositoryTest.php` (5 testow: domyslne dane cechy, whitelist sortowania/paginacji, zapis wartosci i domyslnej, usuwanie pustych tlumaczen, jezyk domyslny)
- `tests/Unit/admin/Controllers/ShopAttributeControllerTest.php` (7 testow: kontrakty metod, brak aliasow legacy, return types, DI konstruktora, walidacja `validateValuesRows`)
## Aktualizacja suite (ShopProductSets refactor, ver. 0.272)
Ostatnio zweryfikowano: 2026-02-15
```text
OK (324 tests, 1000 assertions)
```
Nowe testy dodane 2026-02-15:
- `tests/Unit/Domain/ProductSet/ProductSetRepositoryTest.php` (7 testow: find default/normalize, save insert/update, delete invalid, whitelist sortowania/paginacji, allSets)
- `tests/Unit/admin/Controllers/ShopProductSetsControllerTest.php` (5 testow: kontrakty metod, aliasy legacy, return types, DI konstruktora)
## Aktualizacja suite (ShopProducer refactor, ver. 0.273)
Ostatnio zweryfikowano: 2026-02-15
```text
OK (338 tests, 1063 assertions)
```
Nowe testy dodane 2026-02-15:
- `tests/Unit/Domain/Producer/ProducerRepositoryTest.php` (9 testow: find default/normalize, save insert/update, delete invalid/success, whitelist sortowania/paginacji, allProducers, producerProducts)
- `tests/Unit/admin/Controllers/ShopProducerControllerTest.php` (5 testow: kontrakty metod, aliasy legacy, return types, DI konstruktora)
## Aktualizacja suite (ShopProduct mass_edit, ver. 0.274)
Ostatnio zweryfikowano: 2026-02-15
```text
OK (351 tests, 1091 assertions)
```
Nowe testy dodane 2026-02-15:
- `tests/Unit/Domain/Product/ProductRepositoryTest.php` (rozszerzenie: `allProductsForMassEdit`, `getProductsByCategory`, `applyDiscountPercent`)
- `tests/Unit/admin/Controllers/ShopProductControllerTest.php` (7 testow: kontrakty metod, return types, DI konstruktora)

View File

@@ -0,0 +1,48 @@
<?php
error_reporting( E_ALL ^ E_NOTICE ^ E_STRICT ^ E_WARNING ^ E_DEPRECATED );
define( 'gdir', "/libraries/grid" );
require_once dirname( __FILE__ ) . '/grid-view.php';
require_once dirname( __FILE__ ) . '/gdb.min.php';
require_once dirname( __FILE__ ) . '/grid.php';
require_once dirname( __FILE__ ) . '/grid-edit.php';
require_once dirname( __FILE__ ) . '/grid-upload.php';
session_start();
/* połączenie z bazą ustawić wg własnych preferencji */
require_once dirname( __FILE__ ) . '/../../config.php';
require_once dirname( __FILE__ ) . '/../../autoload/class.S.php';
$legacyFactoryFiles = [
'/../../autoload/admin/factory/class.Articles.php',
'/../../autoload/admin/factory/class.Pages.php',
'/../../autoload/admin/factory/class.ShopProduct.php',
];
foreach ( $legacyFactoryFiles as $legacyFactoryFile )
{
$legacyFactoryPath = dirname( __FILE__ ) . $legacyFactoryFile;
if ( file_exists( $legacyFactoryPath ) )
require_once $legacyFactoryPath;
}
require_once dirname( __FILE__ ) . '/../../autoload/shop/class.Product.php';
require_once dirname( __FILE__ ) . '/../../libraries/medoo/medoo.php';
$gdb = array(
'database_type' => 'mysql',
'database_name' => $database['name'],
'server' => $database['host'],
'username' => $database['user'],
'password' => $database['password'],
'charset' => 'utf8'
);
$mdb = new medoo( [
'database_type' => 'mysql',
'database_name' => $database['name'],
'server' => $database['host'],
'username' => $database['user'],
'password' => $database['password'],
'charset' => 'utf8'
] );

View File

@@ -0,0 +1,80 @@
<?php
namespace Tests\Unit\admin\Controllers;
use PHPUnit\Framework\TestCase;
use admin\Controllers\ShopCategoryController;
use Domain\Category\CategoryRepository;
use Domain\Languages\LanguagesRepository;
class ShopCategoryControllerTest extends TestCase
{
private $repository;
private $languagesRepository;
private $controller;
protected function setUp(): void
{
$this->repository = $this->createMock(CategoryRepository::class);
$this->languagesRepository = $this->createMock(LanguagesRepository::class);
$this->controller = new ShopCategoryController($this->repository, $this->languagesRepository);
}
public function testConstructorAcceptsDependencies(): void
{
$controller = new ShopCategoryController($this->repository, $this->languagesRepository);
$this->assertInstanceOf(ShopCategoryController::class, $controller);
}
public function testHasExpectedActionMethods(): void
{
$this->assertTrue(method_exists($this->controller, 'view_list'));
$this->assertTrue(method_exists($this->controller, 'list'));
$this->assertTrue(method_exists($this->controller, 'category_edit'));
$this->assertTrue(method_exists($this->controller, 'edit'));
$this->assertTrue(method_exists($this->controller, 'save'));
$this->assertTrue(method_exists($this->controller, 'category_delete'));
$this->assertTrue(method_exists($this->controller, 'delete'));
$this->assertTrue(method_exists($this->controller, 'category_products'));
$this->assertTrue(method_exists($this->controller, 'products'));
$this->assertTrue(method_exists($this->controller, 'category_url_browser'));
$this->assertTrue(method_exists($this->controller, 'save_categories_order'));
$this->assertTrue(method_exists($this->controller, 'save_products_order'));
$this->assertTrue(method_exists($this->controller, 'cookie_categories'));
}
public function testViewActionsReturnString(): void
{
$reflection = new \ReflectionClass($this->controller);
$this->assertEquals('string', (string)$reflection->getMethod('view_list')->getReturnType());
$this->assertEquals('string', (string)$reflection->getMethod('list')->getReturnType());
$this->assertEquals('string', (string)$reflection->getMethod('category_edit')->getReturnType());
$this->assertEquals('string', (string)$reflection->getMethod('edit')->getReturnType());
$this->assertEquals('string', (string)$reflection->getMethod('category_products')->getReturnType());
$this->assertEquals('string', (string)$reflection->getMethod('products')->getReturnType());
}
public function testMutationActionsReturnVoid(): void
{
$reflection = new \ReflectionClass($this->controller);
$this->assertEquals('void', (string)$reflection->getMethod('save')->getReturnType());
$this->assertEquals('void', (string)$reflection->getMethod('category_delete')->getReturnType());
$this->assertEquals('void', (string)$reflection->getMethod('delete')->getReturnType());
$this->assertEquals('void', (string)$reflection->getMethod('category_url_browser')->getReturnType());
$this->assertEquals('void', (string)$reflection->getMethod('save_categories_order')->getReturnType());
$this->assertEquals('void', (string)$reflection->getMethod('save_products_order')->getReturnType());
$this->assertEquals('void', (string)$reflection->getMethod('cookie_categories')->getReturnType());
}
public function testConstructorRequiresCategoryAndLanguagesRepositories(): void
{
$reflection = new \ReflectionClass(ShopCategoryController::class);
$constructor = $reflection->getConstructor();
$params = $constructor->getParameters();
$this->assertCount(2, $params);
$this->assertEquals('Domain\\Category\\CategoryRepository', $params[0]->getType()->getName());
$this->assertEquals('Domain\\Languages\\LanguagesRepository', $params[1]->getType()->getName());
}
}

Binary file not shown.