Files
kupshop/admin/lists/SectionsList.php
2025-08-02 16:30:27 +02:00

661 lines
25 KiB
PHP

<?php
use KupShop\AdminBundle\AdminList\BaseList;
use KupShop\AdminBundle\AdminListMassEdit\SectionsListMassEdit;
use KupShop\CatalogBundle\Util\SectionUtil;
use KupShop\I18nBundle\Admin\Util\ListTranslationsFigureBadges;
use KupShop\KupShopBundle\Config;
use KupShop\KupShopBundle\Util\Compat\ServiceContainer;
use KupShop\KupShopBundle\Util\HtmlBuilder\HTML;
use Query\Operator;
class SectionsList extends BaseList
{
use \KupShop\AdminBundle\Util\CategoryTree;
use ListTranslationsFigureBadges;
protected $template = 'list/sections.tpl';
protected $tableName = 'sections';
protected ?string $tableAlias = 's';
protected $showMassEdit = true;
protected $pageDivide = 9999;
protected $tableDef = [
'id' => 's.id',
'fields' => [
'Popis' => ['field' => 'descr', 'render' => 'renderHTML', 'class' => 'alignLeft', 'size' => 3],
'visibility' => ['translate' => true, 'field' => 'figure', 'render' => 'renderVisibility', 'visible' => 'N', 'fieldType' => SectionsList::TYPE_LIST],
'Feed H/G/S/GL' => ['field' => 'join_feed_heureka', 'render' => 'getFeeds', 'class' => 'columnFeeds', 'size' => 2],
'priority' => ['translate' => true, 'field' => 'priority', 'render' => 'renderPriority', 'size' => 1, 'visible' => 'N', 'fieldType' => SectionsList::TYPE_LIST],
'Heureka' => ['field' => 'join_feed_heureka', 'render' => 'renderBoolean', 'visible' => 'N', 'fieldType' => SectionsList::TYPE_LIST_AUTOCOMPLETE, 'fieldOptions' => ['autocomplete' => 'feed_heureka', 'table' => 'kupshop_shared.feed_heureka', 'field' => "CONCAT(name, ' - ' ,category_text)"]],
'Heureka SK' => ['field' => 'join_feed_heureka_sk', 'render' => 'renderBoolean', 'visible' => 'N', 'fieldType' => SectionsList::TYPE_LIST_AUTOCOMPLETE, 'fieldOptions' => ['autocomplete' => 'feed_heureka_sk', 'table' => 'kupshop_shared.feed_heureka_sk', 'field' => "CONCAT(name, ' - ' ,category_text)"]],
'Google' => ['field' => 'join_feed_google', 'render' => 'renderBoolean', 'visible' => 'N', 'fieldType' => SectionsList::TYPE_LIST_AUTOCOMPLETE, 'fieldOptions' => ['autocomplete' => 'feed_google', 'table' => 'kupshop_shared.feed_google', 'field' => "CONCAT(name, ' - ' ,category_text)"]],
'Seznam' => ['field' => 'join_feed_seznam', 'render' => 'renderBoolean', 'visible' => 'N', 'fieldType' => SectionsList::TYPE_LIST_AUTOCOMPLETE, 'fieldOptions' => ['autocomplete' => 'feed_seznam', 'table' => 'kupshop_shared.feed_seznam', 'field' => "CONCAT(name, ' - ' ,category_text)"]],
'Glami' => ['field' => 'join_feed_glami', 'render' => 'renderBoolean', 'visible' => 'N', 'fieldType' => SectionsList::TYPE_LIST_AUTOCOMPLETE, 'fieldOptions' => ['autocomplete' => 'feed_glami', 'table' => 'kupshop_shared.feed_glami', 'field' => "CONCAT(name, ' - ' ,category_text)"]],
'orderby' => ['translate' => true, 'field' => 'orderby', 'render' => 'renderOrderBy', 'visible' => 'N', 'size' => 1.5, 'fieldType' => SectionsList::TYPE_LIST],
'orderdir' => ['translate' => true, 'field' => 'orderdir', 'render' => 'renderOrderDir', 'visible' => 'N', 'size' => 1.5, 'fieldType' => SectionsList::TYPE_LIST],
'Příznak' => ['field' => 'flags', 'render' => 'renderFlags', 'class' => 'columnCampaigns', 'size' => 1],
'Podsekce' => ['field' => 'subsection', 'render' => 'getSubsection', 'class' => 'alignRight hidden-label overflow-visible columnBtns', 'size' => 1],
'redirect_url' => ['translate' => true, 'field' => 'redirect_url', 'visible' => 'N', 'fieldType' => SectionsList::TYPE_STRING, 'size' => 1],
],
'class' => null,
];
protected $showExport = false;
private $openedSections;
protected $additionalData;
protected array $sections = [];
private ?array $slidersCache = null;
protected function getSectionUtil(): SectionUtil
{
static $util = null;
if (!$util) {
$util = ServiceContainer::getService(SectionUtil::class);
}
return $util;
}
protected function getSliderUtil(): ?KupShop\ContentBundle\Util\SliderUtil
{
static $sliderUtil = null;
return $sliderUtil ??= (
findModule(\Modules::SLIDERS)
? ServiceContainer::getService(\KupShop\ContentBundle\Util\SliderUtil::class)
: null
);
}
public function handleGenerateVirtualSections()
{
$sectionUtil = $this->getSectionUtil();
$sectionUtil->generateVirtualSections();
$this->returnOK('Přegenerováno');
}
public function customizeTableDef($tableDef)
{
ini_set('memory_limit', '256M');
$tableDef = parent::customizeTableDef($tableDef);
$cfg = Config::get();
if (findModule(\Modules::TRANSLATIONS)) {
$tableDef['fields']['translationsFigure'] = $this->getTranslationsFigureField(
translationClass: \KupShop\I18nBundle\Translations\SectionsTranslation::class,
column: ['translation_section' => 'translations'],
);
}
$tableDef['fields']['priority']['fieldOptions'] = [
'-1' => translate('priority_minor', 'sections'),
'0' => translate('priority_normal', 'sections'),
'1' => translate('priority_major', 'sections'),
];
$tableDef['fields']['visibility']['fieldOptions'] = [
'Y' => translate('figureY', 'sections'),
'N' => translate('figureN', 'sections'),
'O' => translate('figureO', 'sections'),
];
$tableDef['fields']['orderby']['fieldOptions'] = [
'date' => translate('orderby_date', 'sections'),
'title' => translate('orderby_title', 'sections'),
'price' => translate('orderby_price', 'sections'),
'store' => translate('orderby_store', 'sections'),
'sell' => translate('orderby_sell', 'sections'),
'code' => translate('orderby_code', 'sections'),
'discount' => translate('orderby_discount', 'sections'),
'soldPrice' => translate('orderby_soldPrice', 'sections'),
'storeValue' => translate('orderby_storeValue', 'sections'),
];
$tableDef['fields']['orderdir']['fieldOptions'] = [
'ASC' => translate('orderdir_asc', 'sections'),
'DESC' => translate('orderdir_desc', 'sections'),
];
if (findModule(Modules::INDEXED_FILTER)) {
$tableDef['fields']['filterUrl'] = [
'translate' => true, 'field' => 's.filter_url', 'visible' => 'N', 'fieldType' => SectionsList::TYPE_STRING, 'size' => 1,
];
$tableDef['fields']['title'] = [
'translate' => true, 'field' => 'section_filter_title', 'visible' => 'N', 'size' => 1, 'fieldType' => SectionsList::TYPE_STRING,
];
}
if (findModule(Modules::PRODUCTS_SECTIONS, 'custom_url')) {
$tableDef['fields']['sectionUrl'] = [
'translate' => true, 'field' => 'section_custom_url', 'visible' => 'N', 'fieldType' => SectionsList::TYPE_STRING, 'size' => 1,
];
}
$tableDef['fields']['Popis']['spec'] = function (Query\QueryBuilder $qb) {
$subSelect = sqlQueryBuilder()->select('LEFT(STRIP_TAGS(b.content), 200)')
->from('blocks', 'b')
->where('b.id_root = s.id_block AND b.content IS NOT NULL AND b.content != "" ')
->orderBy('b.position')
->setMaxResults(1);
$qb->addSubselect($subSelect, 'descr');
};
$tableDef['fields']['Heureka']['spec'] = function (Query\QueryBuilder $qb) {
$qb->addSelect('f_h.id as join_feed_heureka')
->leftJoin('s', 'kupshop_shared.feed_heureka', 'f_h', 'f_h.id=s.feed_heureka');
};
$tableDef['fields']['Heureka SK']['spec'] = function (Query\QueryBuilder $qb) {
$qb->addSelect('f_hsk.id as join_feed_heureka_sk')
->leftJoin('s', 'kupshop_shared.feed_heureka_sk', 'f_hsk', 'f_hsk.id=s.feed_heureka_sk');
};
$tableDef['fields']['Google']['spec'] = function (Query\QueryBuilder $qb) {
$qb->addSelect('f_g.id as join_feed_google')
->leftJoin('s', 'kupshop_shared.feed_google', 'f_g', 'f_g.id=s.feed_google');
};
$tableDef['fields']['Seznam']['spec'] = function (Query\QueryBuilder $qb) {
$qb->addSelect('f_s.id as join_feed_seznam')
->leftJoin('s', 'kupshop_shared.feed_seznam', 'f_s', 'f_s.id=s.feed_seznam');
};
$tableDef['fields']['Glami']['spec'] = function (Query\QueryBuilder $qb) {
$qb->addSelect('f_gl.id IS NOT NULL as join_feed_glami')
->leftJoin('s', 'kupshop_shared.feed_glami', 'f_gl', 'f_gl.id=s.feed_glami');
};
$tableDef['fields']['Feed H/G/S/GL']['spec'] = function (Query\QueryBuilder $qb) use ($tableDef) {
$qb->addSelect($tableDef['fields']['Heureka']['spec']);
$qb->addSelect($tableDef['fields']['Google']['spec']);
$qb->addSelect($tableDef['fields']['Seznam']['spec']);
$qb->addSelect($tableDef['fields']['Glami']['spec']);
};
if (findModule(\Modules::SLIDERS)) {
$positions = \KupShop\ContentBundle\Util\SliderUtil::getPositions();
foreach ($positions as $position => $positionName) {
$tableDef['fields']["{$positionName} banner"] = [
'spec' => "JSON_EXTRACT(sliders_positions.positions, '$.{$position}') AS {$position}_slider_name",
'field' => "{$position}_slider_name",
];
}
}
return $tableDef;
}
public function getColumns()
{
$table = parent::getColumns();
// add first column and fix positions
foreach ($table['fields'] as $key => $value) {
$value['position']++;
}
$table['fields'] = array_merge([
'Sekce' => [
'label' => 'Název',
'field' => 'name', 'render' => 'getTitle', 'class' => 'alignLeft', 'size' => 4, 'position' => '0',
'visible' => 'Y', 'multiplier' => '1', 'type_id' => $table['id'], 'type' => $GLOBALS['type'],
],
], $table['fields']);
return $table;
}
public function renderVisibility($values, $column)
{
if ($values['figure'] === 'O') {
return $this->renderIcon('warning');
}
return $this->renderBoolean($values, $column);
}
public function renderFlags($values, $column)
{
$badges = $this->getSectionUtil()->getBadges(
figure: $values['figure'],
virtual: $values['virtual'],
showInSearch: $values['show_in_search']
);
return array_map(
fn (array $badge) => $this->renderBadge(
translate($badge['translate_key'], 'sections'),
$badge['class'],
$badge['icon']
),
$badges,
);
}
public function renderPriority($values, $column)
{
switch ($values[$column['field']]) {
case -1:
return translate('priority_minor', 'sections');
case 0:
return translate('priority_normal', 'sections');
case 1:
return translate('priority_major', 'sections');
}
return '';
}
public function renderOrderBy($values, $column)
{
if ($orderby = $values['orderby'] ?? null) {
return translate('orderby_'.$orderby, 'sections');
}
return '';
}
public function renderOrderDir($values, $column)
{
if ($orderdir = $values['orderdir'] ?? null) {
return translate('orderdir_'.strtolower($orderdir), 'sections');
}
return '';
}
public function getListRowValue($values, $field, $default = null)
{
$field_name = $this->getFieldArrayName($field);
if (empty($values[$field_name])) {
return $this->additionalData[$values['id']][$field_name] ?? $default;
}
return $values[$field_name];
}
public function getTitle($values, $column)
{
if ($values['id'] == 0) {
return HTML::create('strong')->attr('class', 'text-dark')
->text($this->getListRowValue($values, $column['field']));
}
return [
HTML::create('span')
->class('drag-drop-mover')
->tag('i')
->class('bi bi-arrows-move handle')
->end(),
HTML::create('span')
->class('bi bi-dash-circle opener '
.($this->openedSections == 'all' || in_array($values['id'], $this->openedSections) ? ' ' : 'plus ')
.($values['children_count'] > 0 ? '' : 'disabled')),
HTML::create('strong')
->attr('class', 'text-dark')
->text($this->getListRowValue($values, $column['field'])),
];
}
public function getFeeds($values, $column)
{
$return = [];
$return[] = $this->renderBoolean($values, ['field' => 'join_feed_heureka'], 'Heureka');
$return[] = $this->renderBoolean($values, ['field' => 'join_feed_google'], 'Google');
$return[] = $this->renderBoolean($values, ['field' => 'join_feed_seznam'], 'Seznam');
$return[] = $this->renderBoolean($values, ['field' => 'join_feed_glami'], 'Glami');
// add space between icons
/** @var HTML $ret */
foreach ($return as &$ret) {
$ret = $ret->tag('span')->attr('style', 'padding-right:4px')->end();
}
return $return;
}
public function getTemplate()
{
if (getVal('root')) {
return 'list/sections.ajax.tpl';
}
return parent::getTemplate();
}
public function getSubsection($values)
{
if ($values['id'] == 0) {
$result = HTML::create('a')
->attr('class', 'btn btn-sm btn-block btn-success')
->attr('title', 'Přidat sekci')
->attr('href', 'javascript:nw("sections", '.null.');')
->tag('span')
->class('bi bi-plus-lg m-r-1')
->end();
return $result->text('Přidat sekci');
}
return HTML::create('a')
->attr('class', 'btn btn-sm btn-success')
->attr('title', 'Přidat podsekci')
->attr('href', 'javascript:nw("sections", "", "&sections[]='.$values['id'].'");')
->tag('span')
->class('bi bi-plus-lg')
->end();
}
public function handleDrag()
{
$tree = getVal('tree');
if ($tree) {
sqlGetConnection()->transactional(function () use ($tree) {
$old_id_topsection = returnSQLResult('SELECT id_topsection FROM sections_relation WHERE id_section=:id_section', ['id_section' => $tree['id']]);
$compare = ' >= ';
if (!empty($tree['after'])) {
$id_anchor = $tree['after'];
$compare = ' > ';
} elseif (!empty($tree['before'])) {
$id_anchor = $tree['before'];
} else {
$id_anchor = null;
}
if ($id_anchor) {
$anchor = sqlFetchAssoc(sqlQuery('SELECT * FROM sections_relation WHERE id_section=:id_section', ['id_section' => $id_anchor]));
} else {
// Insert as the only child
$anchor = ['position' => 0, 'id_topsection' => $tree['target']];
}
// Check cycle
$cycling = $this->checkSubSections($this->getCategoryById($tree['id'])['children'], $anchor['id_topsection']);
if ($cycling) {
header('HTTP/1.1 500 Error');
exit('Nelze zvolit jako nadřazenou sekci některou z podsekcí!');
}
sqlQuery('DELETE FROM sections_relation WHERE id_section=:id_section', ['id_section' => $tree['id']]);
sqlQueryBuilder()->update('sections_relation')
->set('position', 'position+2')
->where(Operator::equalsNullable(['id_topsection' => $anchor['id_topsection']]))
->andWhere("position {$compare} :anchor_position")
->setParameter('anchor_position', $anchor['position'])
->execute();
sqlQueryBuilder()->insert('sections_relation')
->directValues([
'id_section' => $tree['id'],
'id_topsection' => $anchor['id_topsection'],
'position' => $anchor['position'] + 1,
])
->execute();
self::orderTreeLevel($anchor['id_topsection']);
if ($anchor['id_topsection'] != $old_id_topsection) {
self::orderTreeLevel($old_id_topsection);
}
});
MenuSectionTree::invalidateCache();
exit('OK');
}
exit('Err');
}
public static function orderTreeLevel($id_topsection)
{
if (empty($id_topsection)) {
$where = 'id_topsection IS NULL';
} else {
$where = "id_topsection = {$id_topsection}";
}
$SQL = sqlQuery('SELECT id_section, position
FROM '.getTableName('sections_relation').' sr
LEFT JOIN '.getTableName('sections')." s ON s.id=sr.id_section
WHERE {$where}
ORDER BY sr.position, s.name");
foreach ($SQL as $index => $row) {
if ($row['position'] != $index) {
sqlQuery('UPDATE '.getTableName('sections_relation')." SET position={$index} WHERE id_section={$row['id_section']} AND {$where}");
}
}
}
public function renderCell($values, $column, $tooltip = '')
{
$value = parent::renderCell($values, $column, $tooltip);
if (in_array($column['field'], ['join_feed_heureka', 'join_feed_heureka_sk', 'join_feed_seznam', 'join_feed_google', 'join_feed_glami'])) {
return (bool) $value;
}
return $value;
}
public function getQuery()
{
$qb = $this->getSectionsQueryBuilder()
->andWhere(Operator::inIntArray($this->getFilterIds(), 's.id'));
return $qb;
}
public function getFilterQuery(): Query\QueryBuilder
{
$qb = parent::getFilterQuery()
->leftJoin('s', 'sections_relation', 'sr', 's.id = sr.id_section');
if ($rootId = getVal('root') ?? false) {
$qb->andWhere(Operator::equals(['sr.id_topsection' => $rootId]));
} else {
$qb->andWhere(
Operator::orX(
Operator::equals(['sr.id_topsection' => 0]),
'sr.id_topsection IS NULL'
)
);
}
return $qb;
}
public function get_vars()
{
$vars = parent::get_vars();
$vars['openedSections'] = $this->getOpenedSections();
return $vars;
}
public function getSQL(Query\QueryBuilder $qb)
{
$result = parent::getSQL($qb);
$sections = $this->loadSubSections(
sqlFetchAll($result['SQL'], 'id')
);
$result['SQL'] = $sections;
return $result;
}
protected function getDefaultOrder()
{
return [
'sort' => 'sr.position',
'direction' => 'ASC',
];
}
protected function getSectionsQueryBuilder(): Query\QueryBuilder
{
$qb = sqlQueryBuilder()
->select('s.*')
->from('sections', 's')
->leftJoin('s', 'sections_relation', 'sr', 's.id = sr.id_section')
->groupBy('s.id')
->orderBy('sr.position');
$childrenCountSubQuery = sqlQueryBuilder()
->select('COUNT(id_section)')
->from('sections_relation')
->where('id_topsection = s.id');
$qb->addSubselect($childrenCountSubQuery, 'children_count');
if (findModule(Modules::INDEXED_FILTER)) {
$qb->addSelect('s.title as section_filter_title, s.filter_url');
}
if (findModule(Modules::PRODUCTS_SECTIONS, 'custom_url')) {
$qb->addSelect('s.url as section_custom_url');
}
if (findModule(\Modules::SLIDERS)) {
$sliderPositions = sqlQueryBuilder()->select('sis.id_section AS id', 'CONCAT("{", GROUP_CONCAT( DISTINCT CONCAT(\'"\', sis.position, \'": "\', sliders.name, \'"\') SEPARATOR \', \'), "}") AS positions')
->from('sliders_in_sections', 'sis')
->leftJoin('sis', 'sliders', 'sliders', 'sis.id_slider = sliders.id')
->groupBy('sis.id_section');
$qb->leftJoinSubQuery('s', $sliderPositions, 'sliders_positions', 's.id = sliders_positions.id');
}
return $qb;
}
private function loadSubSections(array $rootSections): array
{
$openedSections = $this->getOpenedSections();
if (empty($openedSections)) {
return $rootSections;
}
$sectionUtil = ServiceContainer::getService(SectionUtil::class);
if ($openedSections === 'all') {
$sectionIds = call_user_func_array('array_merge', array_map(
fn ($x) => $sectionUtil->getDescendantCategories($x['id'], false),
$rootSections
));
} else {
$sectionIds = array_keys($rootSections);
foreach ($openedSections as $openedSection) {
$sectionIds = array_merge($sectionIds, $sectionUtil->getDescendantCategories($openedSection, false));
}
}
$qb = $this->getSectionsQueryBuilder()
->addSelect('sr.id_topsection')
->andWhere(
Operator::orX(
Operator::inIntArray($sectionIds, 'sr.id_topsection'),
'sr.id_topsection IS NULL'
)
)
->orderBy('FIELD(s.id, :sectionIds)')
->setParameter('sectionIds', $sectionIds, \Doctrine\DBAL\Connection::PARAM_INT_ARRAY);
$qb = $this->evaluateColumnClosures($qb);
return $this->buildSectionTree(
sqlFetchAll($qb->execute(), 'id')
);
}
// zbuildim strom sekci za pomoci referenci, protoze je to mnohem rychlejsi, jak kdyz se to buildi rekurzivne
private function buildSectionTree(array $sections): array
{
$tree = [];
$references = [];
foreach ($sections as $id => &$section) {
// Add the node to our associative array using its ID as key
$references[$id] = &$section;
// Add empty placeholder for children
$section['children'] = [];
// If it's a root node, we add it directly to the tree
if (empty($section['id_topsection'])) {
$tree[$id] = &$section;
} else {
// It was not a root node, add this node as a reference in the parent.
$references[$section['id_topsection']]['children'][$id] = &$section;
}
}
return $tree;
}
private function getOpenedSections()
{
if ($this->openedSections !== null) {
return $this->openedSections;
}
$openedSectionsGet = getVal('opened') ?? [];
$openedSectionsCookie = getVal('products_list_opened_sections', $_COOKIE, false);
if (isset($_COOKIE['products_list_opened_sections'])) {
unset($_COOKIE['products_list_opened_sections']);
setcookie('products_list_opened_sections', '', -1, '/');
}
$this->openedSections = [];
if ($openedSectionsGet != 'all') {
$this->openedSections += ($openedSectionsGet) ? json_decode_strict($openedSectionsGet) : [];
$this->openedSections += ($openedSectionsCookie) ? json_decode_strict($openedSectionsCookie) : [];
} else {
$this->openedSections = 'all';
}
return $this->openedSections;
}
public function customizeMassTableDef($tableDef)
{
$tableDef = parent::customizeMassTableDef($tableDef);
if (findModule(\Modules::SLIDERS)) {
$positions = \KupShop\ContentBundle\Util\SliderUtil::getPositions();
foreach ($positions as $position => $positionName) {
$tableDef['fields']["{$positionName} banner"] = [
'fieldType' => SectionsListMassEdit::TYPE_SLIDERS,
'sliderPosition' => $position,
'field' => 'sliders_'.$position,
'size' => 1.25,
];
}
$badges = $tableDef['fields']['Příznak'];
unset($tableDef['fields']['Příznak']);
$tableDef['fields']['Příznak'] = $badges;
$tableDef['fields']['Popis']['size'] = 2;
}
unset($tableDef['fields']['Feed H/G/S/GL']);
return $tableDef;
}
}