first commit

This commit is contained in:
2025-08-02 16:30:27 +02:00
commit 23646bfcee
14851 changed files with 1750626 additions and 0 deletions

View File

@@ -0,0 +1,109 @@
<?php
declare(strict_types=1);
namespace KupShop\CatalogBundle\Admin\Controller;
use KupShop\CatalogBundle\Query\Search;
use KupShop\KupShopBundle\Routing\AdminRoute;
use Query\Operator as Op;
use Query\QueryBuilder;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
class SectionsAutocompleteController extends AbstractController
{
#[AdminRoute(path: '/autocomplete/sections/{id}', methods: ['GET'])]
public function getSubsections(Request $request, ?int $id = null): Response
{
if ($request->query->has('term')) {
return $this->json($this->autocomplete($request->query->get('term')));
}
if ($request->query->get('all_subsections') == 1) {
$qb = $this->getRecursiveQuery($id, 's');
} else {
$qb = sqlQueryBuilder()
->from('sections_relation', 'ss')
->leftJoin('ss', 'sections', 's', 's.id = ss.id_section');
if (!isset($id)) {
$qb->andWhere('s.id IN (SELECT DISTINCT id_section FROM sections_relation WHERE id_topsection IS NULL OR id_topsection = 0)');
} else {
$qb->andWhere(Op::equals(['id_topsection' => $id]));
}
}
$qb->addSelect('s.id', 's.name', $this->hasSubsections('s.id'))
->andWhere("s.figure = 'Y'")
->addOrderBy('position');
return $this->json($qb->execute()->fetchAllAssociative());
}
public function autocomplete(string $term): array
{
$qb = $this->getRecursiveQuery()
->select(
'cte.id',
'cte.name',
'cte.path as full_path',
$this->hasSubsections('cte.id'),
)
->andWhere(Search::searchFields($term, [['field' => 'cte.path', 'match' => 'both']], 'OR'))
->setMaxResults(30);
return $qb->execute()->fetchAllAssociative();
}
protected function getRecursiveQuery(?int $idTopSection = null, string $alias = 'cte'): QueryBuilder
{
$whereSection = $idTopSection ? 'id_topsection = :idTopSection' : 'id_topsection IS NULL';
$recursiveQuery = "
WITH RECURSIVE cte (id, id_topsection, path, name, figure, position, depth, full_position) as (
SELECT id_section,
id_topsection,
(SELECT name FROM sections WHERE id = id_section LIMIT 1) as path,
(SELECT name FROM sections WHERE id = id_section LIMIT 1) as name,
(SELECT figure FROM sections WHERE id = id_section LIMIT 1) as figure,
position,
1 AS depth,
CAST(LPAD(position, 5, 0) AS CHAR(500)) AS full_position
FROM sections_relation
WHERE {$whereSection}
UNION ALL
SELECT sr.id_section,
sr.id_topsection,
CONCAT(cte.path,' > ',(SELECT name FROM sections WHERE id = sr.id_section LIMIT 1)) as path,
(SELECT name FROM sections WHERE id = sr.id_section LIMIT 1) as name,
(SELECT figure FROM sections WHERE id = sr.id_section LIMIT 1) as figure,
sr.position,
depth + 1,
CONCAT(full_position, '/', LPAD(sr.position, 5, 0))
FROM sections_relation sr
INNER JOIN cte
on sr.id_topsection = cte.id
) SELECT * FROM cte";
$qb = sqlQueryBuilder()->from('('.$recursiveQuery.')', $alias);
if ($idTopSection !== null) {
$qb->setParameter('idTopSection', $idTopSection);
}
return $qb;
}
private function hasSubsections(string $idField = 'id', string $as = 'has_subsections'): string
{
return "EXISTS (
SELECT *
FROM sections_relation _ss
LEFT JOIN sections _s ON _s.id = _ss.id_section
WHERE {$idField} = _ss.id_topsection
AND _s.figure = 'Y'
) AS {$as}";
}
}

View File

@@ -0,0 +1,56 @@
<?php
namespace KupShop\CatalogBundle\Admin;
use KupShop\CatalogBundle\ProductList\ProductList;
use KupShop\CatalogBundle\Util\ProductsFilterSpecs;
use KupShop\KupShopBundle\Util\Compat\ServiceContainer;
class ProductsFilter extends \Window
{
protected $template = 'window/productsFilter.tpl';
public function get_vars()
{
$vars = parent::get_vars();
$vars['filter'] = json_decode(getVal('filter'), true);
$vars['blocekProducts'] = getVal('callback') == 'blocek_setProducts';
$vars['productsSortable'] = $vars['blocekProducts'];
return $vars;
}
public function handleLoadProducts()
{
$filter = getVal('filter');
$filter = \KupShop\AdminBundle\Admin\ProductsFilter::cleanFilter($filter);
$productList = ServiceContainer::getService(ProductList::class);
$productsFilterSpecs = ServiceContainer::getService(ProductsFilterSpecs::class);
$filterSpecs = $productsFilterSpecs->getSpecs($filter);
$productList->andSpec($filterSpecs);
$productList->fetchImages('admin');
$productList->limit(20);
$result = [];
$result['count'] = 0;
$result['filter'] = json_encode($filter);
foreach ($productList->getProducts($result['count']) as $product) {
/* @var $product \Product */
$result['products'][] = [
'id' => $product->id,
'title' => $product->title,
'code' => $product->code,
'inStore' => $product->inStore,
'photo' => $product->image['src'] ?? false,
];
}
header('Content-Type: application/json');
echo json_encode($result);
exit;
}
}
return ProductsFilter::class;

View File

@@ -0,0 +1,326 @@
<?php
declare(strict_types=1);
namespace KupShop\CatalogBundle\Admin\Tabs;
use KupShop\AdminBundle\Admin\WindowTab;
use KupShop\CatalogBundle\Repository\SectionsRepository;
use KupShop\CatalogBundle\Section\SectionFilter;
use KupShop\CatalogBundle\Util\FilterUtil;
use Query\Operator;
use Query\QueryBuilder;
class OrderableSectionFilters extends WindowTab
{
protected $title = 'flapSearch';
protected $template = 'window/sections.filters.tpl';
/** @var 'Y'|'N' */
private string $inherit;
public function __construct(
private readonly FilterUtil $filterUtil,
private readonly SectionsRepository $sectionsRepository,
) {
}
public function getVars($smarty_tpl_vars): array
{
return [
'ID' => $this->getID(),
'filterItems' => $this->getFilterItems(),
'type' => $this->getWindow()->getName(),
'inherit' => $this->getInherit(),
];
}
public function handleUpdate(): void
{
$data = getVal('data');
if (!$data) {
return;
}
$inherit = getVal('inherit', $data, 'N');
$this->saveInherit($inherit);
$filters = getVal('filters', $data);
if ($inherit === 'Y') {
foreach ($filters as &$filter) {
$filter['enabled'] = 'N';
}
}
if ($filters === null) {
return;
}
sqlGetConnection()->transactional(function () use ($filters) {
$this->filterUtil->updateSectionFilters($filters);
});
}
/**
* @throws \Doctrine\DBAL\Driver\Exception
* @throws \Doctrine\DBAL\Exception
*/
public function getFilterItems(): array
{
if ($this->isSection()) {
$section = $this->sectionsRepository->findById((int) $this->getID());
$filterItems = $this->filterUtil->getSectionFilters($section, null);
} else {
$filterItems = $this->filterUtil->getSectionFilters(null, (int) $this->getID());
}
$customFilters = [
SectionFilter::TYPE_PRICE_RANGE => false,
SectionFilter::TYPE_SEARCH => false,
SectionFilter::TYPE_IN_STORE => false,
];
if (findModule(\Modules::LABELS)) {
$customFilters[SectionFilter::TYPE_LABELS] = false;
}
if (findModule(\Modules::SELLERS)) {
$customFilters[SectionFilter::TYPE_SELLERS] = false;
}
if (!$this->isProducer()) {
$customFilters[SectionFilter::TYPE_PRODUCERS] = false;
}
foreach ($filterItems as $filterItem) {
if (array_key_exists($filterItem['type'], $customFilters)) {
$customFilters[$filterItem['type']] = true;
}
}
foreach ($customFilters as $type => $has) {
if ($has) {
continue;
}
$filterItems[] = [
'type' => $type,
'indexing_allowed' => false,
];
}
$parameters = $this->createParametersQueryBuilder();
$variations = $this->createVariationsQueryBuilder();
return array_merge(
$filterItems,
$parameters->execute()->fetchAllAssociative(),
$variations->execute()->fetchAllAssociative(),
);
}
private function createVariationsQueryBuilder(): QueryBuilder
{
$variationType = SectionFilter::TYPE_VARIATION;
$variationsSelects = [
'id' => 'NULL',
'id_source_section' => 'NULL',
'id_source_producer' => 'NULL',
'id_parameter' => 'NULL',
'id_variation_label' => 'pvcl.id',
'type' => "'{$variationType}'",
'position' => 'NULL',
'enabled' => "'N'",
'name' => 'pvcl.label',
];
if ($this->isProducer()) {
$variationsSelects['indexing_allowed'] = '0';
}
if (findModule(\Modules::INDEXED_FILTER) && $this->isSection()) {
$variationsSelects = array_merge($variationsSelects, [
'indexing' => 'pvs.indexing',
'to_title' => 'pvs.to_title',
]);
}
if (findModule(\Modules::CONVERTORS) && $this->isSection()) {
$variationsSelects['convertor'] = 'pvs.convertor';
}
if ($this->isProducer()) {
$condition = 'id_source_producer = :id_producer';
} else {
$condition = 'id_source_section = :id_section';
}
$variations = sqlQueryBuilder()
->select($this->sqlAliasedArray($variationsSelects))
->from('products_variations_choices_labels', 'pvcl')
->andWhere("NOT EXISTS (SELECT 1 FROM sections_filters WHERE id_variation_label = pvcl.id AND {$condition})");
if ($this->isSection()) {
$variations->leftJoin('pvcl',
'products_variations_sections',
'pvs',
'pvs.id_label = pvcl.id AND pvs.id_section = :id_section',
);
$variations->setParameter('id_section', $this->getID());
} else {
$variations->setParameter('id_producer', $this->getID());
}
return $variations;
}
private function createParametersQueryBuilder(): QueryBuilder
{
$parameterType = SectionFilter::TYPE_PARAMETER;
$parameterName = <<<__SQL__
IF(p.unit <> '',
CONCAT(
p.name,
' [', REPLACE(p.unit, '|', ', '), ']'
),
p.name
)
__SQL__;
$parametersSelects = [
'id' => 'NULL',
'id_source_section' => 'NULL',
'id_source_producer' => 'NULL',
'id_parameter' => 'p.id',
'id_variation_label' => 'NULL',
'type' => "'{$parameterType}'",
'position' => 'NULL',
'enabled' => "'N'",
'name' => $parameterName,
'value_type' => 'p.value_type',
];
if ($this->isProducer()) {
$parametersSelects['indexing_allowed'] = '0';
}
if (findModule(\Modules::INDEXED_FILTER) && $this->isSection()) {
$parametersSelects = array_merge($parametersSelects, [
'indexing' => 'ps.indexing',
'to_title' => 'ps.to_title',
]);
}
if (findModule(\Modules::CONVERTORS) && $this->isSection()) {
$parametersSelects['convertor'] = 'NULL';
}
if ($this->isProducer()) {
$condition = 'id_source_producer = :id_producer';
} else {
$condition = 'id_source_section = :id_section';
}
$parameters = sqlQueryBuilder()
->select($this->sqlAliasedArray($parametersSelects))
->from('parameters', 'p')
->andWhere("p.figure = 'Y'")
->andWhere("NOT EXISTS (SELECT 1 FROM sections_filters WHERE id_parameter = p.id AND {$condition})");
if ($this->isSection()) {
$parameters->leftJoin('p',
'parameters_sections',
'ps',
'ps.id_parameter = p.id AND ps.id_section = :id_section',
);
$parameters->setParameter('id_section', $this->getID());
} else {
$parameters->leftJoin('p',
'parameters_producers',
'ps',
'ps.id_parameter = p.id AND ps.id_producer = :id_producer',
);
$parameters->setParameter('id_producer', $this->getID());
}
return $parameters;
}
private function sqlAliasedArray(array $selects): array
{
return array_map(
static fn ($alias, $field) => "{$field} AS {$alias}",
array_keys($selects),
$selects,
);
}
public static function isAllowed(): bool
{
return FilterUtil::useOrderableFilters();
}
public static function getTypes(): array
{
return ['sections' => 1, 'producers' => 1];
}
public function getLabel(): string
{
return translate($this->title, 'sections');
}
public function isProducer(): bool
{
return $this->getWindow()->getName() === 'producers';
}
public function isSection(): bool
{
return $this->getWindow()->getName() === 'sections';
}
private function getInherit(): string
{
if (isset($this->inherit)) {
return $this->inherit;
}
$key = FilterUtil::INHERIT_FILTER_SETTINGS_KEY;
$query = sqlQueryBuilder()
->select("COALESCE(JSON_EXTRACT(data, '$.{$key}'), 'N') AS inherit")
->andWhere(Operator::equals(['id' => $this->getID()]));
if ($this->isProducer()) {
$query->from('producers');
} else {
$query->from('sections');
}
if ($result = $query->execute()->fetchOne()) {
return $this->inherit = json_decode($result) ?? 'N';
}
return $this->inherit = 'N';
}
private function saveInherit(string $inherit): void
{
$key = FilterUtil::INHERIT_FILTER_SETTINGS_KEY;
$query = sqlQueryBuilder()
->set('data', "JSON_SET(data, '$.{$key}', :inherit)")
->andWhere(Operator::equals(['id' => $this->getID()]))
->setParameter('inherit', $inherit);
if ($this->isProducer()) {
$query->update('producers');
} else {
$query->update('sections');
}
$query->execute();
}
}

View File

@@ -0,0 +1,20 @@
<?php
namespace KupShop\CatalogBundle\Admin\Tabs;
use KupShop\AdminBundle\Admin\WindowTab;
class ProducersGPSR extends WindowTab
{
use \DatabaseCommunication;
protected $title = 'flapGPSR';
protected $template = 'window/producersGPSR.tpl';
public static function getTypes()
{
return [
'producers' => 1,
];
}
}

View File

@@ -0,0 +1,118 @@
<?php
namespace KupShop\CatalogBundle\Admin\Tabs;
use KupShop\AdminBundle\Admin\WindowTab;
use KupShop\AdminBundle\AdminBlocksTrait;
use KupShop\AdminBundle\Util\BlocksHistory;
use KupShop\ContentBundle\Util\BlocksTrait;
class ProductsDescriptionPlus extends WindowTab
{
use \DatabaseCommunication;
use BlocksTrait;
use AdminBlocksTrait;
protected $title = 'flapBlocks';
protected $template = 'window/products.description.plus.tpl';
protected BlocksHistory $blocksHistory;
public static function getTypes()
{
return [
'products' => 1,
];
}
public static function isAllowed()
{
return findModule(\Modules::PRODUCTS, \Modules::SUB_DESCR_PLUS);
}
public function getVars($smarty_tpl_vars)
{
$blockID = $this->selectSQL('products', ['id' => $this->getID()], ['id_block'])->fetchColumn();
if ($blockID) {
$blocks = $this->getBlocks((int) $blockID);
$history = $this->blocksHistory->getBlocksHistory($blockID);
} else {
$blocks = [];
$history = [];
}
$photos = sqlQueryBuilder()->select('id_photo')->from('photos_products_descr_plus_relation')
->where(\Query\Operator::equals(['id_product' => $this->getID()]))
->orderBy('position')->execute()->fetchAll();
return [
'blocks' => $blocks,
'photos' => $photos,
'blocks_history' => $history,
];
}
public function handleCopyDescription()
{
if ($productID = getVal('copy_id')) {
$copyRootBlockID = $this->selectSQL('products', ['id' => $productID], ['id_block'])->fetchColumn();
if ($copyRootBlockID) {
$blockID = $this->duplicateBlock($copyRootBlockID);
$this->updateSQL('products', ['id_block' => $blockID], ['id' => $this->getID()]);
$data = [
'id' => $this->getID(),
'copy_id' => $productID,
'count' => returnSQLResult('SELECT COUNT(*) FROM photos_products_descr_plus_relation WHERE id_product=:field', ['field' => $this->getID()]),
];
sqlQuery(
"REPLACE INTO photos_products_descr_plus_relation
(id_photo, id_product, date_added, show_in_lead, position)
SELECT id_photo, :id, NOW(), 'N', position+:count
FROM photos_products_descr_plus_relation WHERE id_product = :copy_id",
$data
);
\Photos::checkLeadPhoto('photos_products_descr_plus_relation', 'id_product', $this->getID());
}
}
$this->window->returnOK();
}
public function handleUpdate()
{
if ($this->getAction() == 'edit') {
// historii je treba ulozit jeste pred ulozenim bloku
$this->blocksHistory->saveBlocksHistory(getVal('blocks', $this->getGlobalData(), []));
}
$this->saveBlocks($this->getGlobalData(), $this->getID(), 'products');
}
public function getGlobalData()
{
return getVal('data', null, []);
}
public function getLabel()
{
return translate($this->title, 'products');
}
public function returnError($ErrStr, $parentRefresh = '', $ID = null)
{
$this->window->returnError($ErrStr, $parentRefresh, $ID);
}
/**
* @required
*/
public function setBlocksHistory(BlocksHistory $blocksHistory): void
{
$this->blocksHistory = $blocksHistory;
}
}

View File

@@ -0,0 +1,45 @@
<?php
namespace KupShop\CatalogBundle\Admin\Tabs;
use KupShop\AdminBundle\Admin\WindowTab;
use Query\Operator;
class ProductsDescriptionPlusImages extends WindowTab
{
use \DatabaseCommunication;
protected $title = 'flapDescriptionPlusPhotos';
protected $template = 'window/products.description.plus.photos.tpl';
public static function getTypes()
{
return [
'products' => 1,
];
}
public static function isAllowed()
{
return findModule(\Modules::PRODUCTS, \Modules::SUB_DESCR_PLUS);
}
public function isVisible()
{
$qb = sqlQueryBuilder()
->select('COUNT(*)')
->from('photos_products_descr_plus_relation')
->where(Operator::equals(['id_product' => getVal('ID')]))
->execute();
if (empty($qb->fetchColumn())) {
return false;
}
return $this->getAction() != 'add';
}
public function getLabel()
{
return translate($this->title, 'products');
}
}

View File

@@ -0,0 +1,44 @@
<?php
namespace KupShop\CatalogBundle\Admin\Tabs;
use KupShop\AdminBundle\Admin\ProductsFilter;
use KupShop\AdminBundle\Admin\WindowTab;
class VirtualCategorySettings extends WindowTab
{
protected $title = 'flapVirtualCategory';
protected $template = 'window/sections.virtual.tpl';
public static function getTypes()
{
return [
'sections' => 1,
];
}
public function isVisible()
{
return ($this->getAction() !== 'remove') && (getVal('ID') > 0);
}
public function handleUpdate()
{
$virtual = getVal('data')['virtual'] ?? null;
if (getVal('Submit') && ($virtual == 'Y')) {
$customData = $this->window->getCustomData();
$filter = getVal('filter');
$customData['virtual_settings'] = ProductsFilter::cleanFilter($filter);
$this->window->setCustomData($customData);
}
}
/**
* @return string
*/
public function getLabel()
{
return translate($this->title, 'sections');
}
}

View File

@@ -0,0 +1,15 @@
<?php
$txt_str['parameterValues'] = [
'titleEdit' => 'Editace hodnoty parametru',
'titleAdd' => 'Přidat hodnotu parametru',
'flapParameterValue' => 'Hodnota parametru',
'id' => 'ID',
'actions' => 'Akce',
'value' => 'Hodnota',
'description' => 'Popis',
'position' => 'Pozice',
'filterUrl' => 'URL filtru',
'title' => 'Nadpis',
];

View File

@@ -0,0 +1,18 @@
<?php
$txt_str['producersGPSR'] = [
'flapGPSR' => 'GPSR kontakt',
'GPSR' => 'GPSR',
'company_name' => 'Název společnosti',
'company_street' => 'Ulice',
'company_city' => 'Město',
'company_zip' => 'PSČ',
'company_country' => 'Země',
'company_email' => 'E-mail',
'company_phone' => 'Telefon',
'company_web' => 'Web kontaktní',
'producer' => 'Výrobce',
'importer' => 'Dovozce do EU',
];

View File

@@ -0,0 +1,21 @@
<?php
$txt_str['producersGPSRList'] = [
'company_name' => 'V: Název společnosti',
'company_street' => 'V: Ulice',
'company_city' => 'V: Město',
'company_zip' => 'V: PSČ',
'company_country' => 'V: Země',
'company_email' => 'V: E-mail',
'company_phone' => 'V: Telefon',
'company_web' => 'V: Web',
'import_company_name' => 'D: Název společnosti',
'import_company_street' => 'D: Ulice',
'import_company_city' => 'D: Město',
'import_company_zip' => 'D: PSČ',
'import_company_country' => 'D: Země',
'import_company_email' => 'D: E-mail',
'import_company_phone' => 'D: Telefon',
'import_company_web' => 'D: Web',
];

View File

@@ -0,0 +1,15 @@
<?php
$txt_str['parameterValues'] = [
'titleEdit' => 'Edit parameter value',
'titleAdd' => 'Add parameter value',
'flapParameterValue' => 'Parameter value',
'id' => 'ID',
'actions' => 'Actions',
'value' => 'Value',
'description' => 'Description',
'position' => 'Position',
'filterUrl' => 'Filter URL',
'title' => 'Title',
];

View File

@@ -0,0 +1,17 @@
<?php
$txt_str['producersGPSR'] = [
'flapGPSR' => 'GPSR contact',
'company_name' => 'Company name',
'company_street' => 'Street address',
'company_city' => 'City',
'company_zip' => 'ZIP code',
'company_country' => 'Country',
'company_email' => 'Email',
'company_phone' => 'Phone',
'company_web' => 'Web',
'producer' => 'Producer',
'importer' => 'Importer to EU',
];

View File

@@ -0,0 +1,21 @@
<?php
$txt_str['producersGPSRList'] = [
'company_name' => 'P: Company name',
'company_street' => 'P: Street',
'company_city' => 'P: City',
'company_zip' => 'P: ZIP',
'company_country' => 'P: Country',
'company_email' => 'P: Email',
'company_phone' => 'P: Phone',
'company_web' => 'P: Web',
'import_company_name' => 'I: Company name',
'import_company_street' => 'I: Street',
'import_company_city' => 'I: City',
'import_company_zip' => 'I: ZIP',
'import_company_country' => 'I: Country',
'import_company_email' => 'I: Email',
'import_company_phone' => 'I: Phone',
'import_company_web' => 'I: Web',
];

View File

@@ -0,0 +1,233 @@
<?php
declare(strict_types=1);
namespace KupShop\CatalogBundle\Admin\lists;
use KupShop\AdminBundle\AdminList\BaseList;
use KupShop\CatalogBundle\Query\Search;
use KupShop\KupShopBundle\Util\HtmlBuilder\HTML;
use Query\Operator;
use Query\QueryBuilder;
class ParameterValuesList extends BaseList
{
use \AdminListSortable;
protected $template = 'listSortable.tpl';
protected $tableDef = [
'id' => 'pl.id',
'fields' => [
'position' => ['translate' => true, 'field' => 'pl.position', 'size' => 0.5, 'render' => 'renderPosition', 'fieldType' => self::TYPE_POSITION],
'id' => ['translate' => true, 'field' => 'pl.id', 'size' => 0.5],
'value' => ['translate' => true, 'field' => 'pl.value', 'fieldType' => self::TYPE_STRING],
'description' => ['translate' => true, 'label' => 'getDescriptionLabel', 'field' => 'pl.description', 'fieldType' => self::TYPE_STRING, 'render' => 'renderDescription'],
'progress' => ['translate' => true, 'translation_section' => 'parameters', 'field' => 'pl.description', 'render' => 'renderProgress'],
'filterUrl' => ['translate' => true, 'field' => 'pl.filter_url', 'fieldType' => self::TYPE_STRING],
'title' => ['translate' => true, 'field' => 'pl.title', 'fieldType' => self::TYPE_STRING],
'actions' => ['translate' => true, 'field' => 'pl.id', 'render' => 'renderActions'],
],
];
protected $tableName = 'parameters_list';
protected ?string $tableAlias = 'pl';
protected $showMassEdit = true;
public function customizeTableDef($tableDef)
{
$tableDef = parent::customizeTableDef($tableDef);
if (!findModule(\Modules::INDEXED_FILTER)) {
unset($tableDef['fields']['filterUrl'], $tableDef['fields']['title']);
}
if ($this->getMeaning() != 'progress') {
unset($tableDef['fields']['progress']);
}
return $tableDef;
}
public function getDescriptionLabel($column, $label)
{
$meaning = $this->getMeaning();
if (in_array($meaning, ['image', 'color'])) {
return translate($meaning, 'parameters');
}
return $label;
}
public function getQuery()
{
$qb = parent::getQuery();
$qb->addSelect('COUNT(pp.id) as cnt')
->leftJoin('pl', 'parameters_products', 'pp', 'pp.value_list = pl.id AND pp.id_parameter = pl.id_parameter')
->orderBy('pl.position, pl.name', 'ASC')
->groupBy('pl.id');
return $qb;
}
private function modifyBySearch(QueryBuilder $qb): void
{
$param = getVal('searchTerm');
if ($param) {
$qb->andWhere(
Search::searchFields($param, [
['field' => 'value', 'match' => 'both'],
['field' => 'description', 'match' => 'both'],
], 'OR')
);
}
}
public function getFilterQuery(): QueryBuilder
{
$qb = parent::getFilterQuery();
if ($parameterId = getVal('parameterId')) {
$qb->andWhere(Operator::equals(['id_parameter' => $parameterId]));
}
$this->modifyBySearch($qb);
return $qb;
}
public function renderActions(array $values): HTML
{
return HTML::create('div')
->class('btn-group')
// counts button
->tag('a')
->attr('href', "javascript:nw('productsList', '', 'parameter_value={$values['id']}&showOld=1')")
->class('btn btn-sm btn-secondary')
->text((string) ($values['cnt'] ?: '0'))
->end()
// merge button
->tag('a')
->attr('href', "javascript:nw('parameters', '', 'acn=merge&ID={$values['id_parameter']}&id_value={$values['id']}&autoclose=1')")
->class('btn btn-sm btn-warning')
->tag('span')
->class('bi bi-arrow-left-right')
->end()
->end()
// delete button
->tag('a')
->attr('href', "/admin/launch.php?s=list.php&type=parameterValues&parameterId=83&acn=deleteValue&delete={$values['id']}")
->class('btn btn-sm btn-danger')
->tag('span')
->class('bi bi-trash')
->end()
->end();
}
public function renderDescription($values, $column): ?HTML
{
$value = $this->renderCell($values, $column);
switch ($this->getMeaning()) {
case 'image':
return HTML::create('a')
->attr('href', $value)
->attr('target', '_blank')
->tag('img')
->class('img-rounded')
->attr('src', $value)
->attr('style', 'width: 50px; height: 50px; object-fit: cover;')
->end()
->end();
case 'color':
$background = "background-color: {$value}";
$data = json_decode($values['data'] ?? '', true);
if (($data['enable_multicolor'] ?? 'N') == 'Y') {
$background = 'background: url(/static/images/color-multi.jpg)';
if (count($data['colors']) == 2) {
$background = "background: linear-gradient(135deg, {$data['colors'][1]} 50%, {$data['colors'][2]} 50%)";
}
}
return HTML::create('div')
->attr('style', "width: 30px; height: 30px; {$background}")
->end();
}
return HTML::create('span')->text($value);
}
public function renderProgress($values, $column): ?HTML
{
$data = json_decode($values['data'] ?? '', true);
if (empty($data['progress']['value']) || empty($data['progress']['max'])) {
return null;
}
$html = HTML::create('div')
->class('progress')
->attr('style', 'width: 200px; height: 10px; display: flex; justify-content: space-between;');
foreach (range(1, $data['progress']['max']) as $item) {
$color = ($item <= $data['progress']['value']) ? '#AAAAAA' : '#DDDDDD';
$html->tag('div')
->attr('style', "width: 100%; height: 10px; margin: 0 1px; border-radius: 3px; background-color: {$color}")
->end();
}
return $html->end();
}
public function handleDrag(): void
{
$url = $_POST['url'] ?? null;
$parsed = [];
if ($url) {
parse_str($url, $parsed);
}
$extraWhere = '';
if ($parameterId = getVal('parameterId', $parsed)) {
$extraWhere = " AND id_parameter = {$parameterId}";
}
$this->saveList(getVal('moved_item'), 'parameters_list', ['position', 'value'], $extraWhere);
exit(json_encode(['result' => true]));
}
public function handleDeleteValue(): void
{
if ($valueId = getVal('delete')) {
sqlQueryBuilder()
->delete('parameters_list')
->where(Operator::equals(['id' => $valueId]))
->execute();
}
redirect($_SERVER['HTTP_REFERER']);
}
public function getMeaning()
{
if (!$parameterId = getVal('parameterId')) {
return null;
}
if (isset($this->meaning)) {
return $this->meaning;
}
return $this->meaning = sqlQueryBuilder()
->select('value_meaning')
->from('parameters')
->where(Operator::equals(['id' => $parameterId]))
->execute()->fetchOne();
}
}
return ParameterValuesList::class;

View File

@@ -0,0 +1,624 @@
<?php
namespace KupShop\CatalogBundle\Admin\lists;
use KupShop\AdminBundle\AdminList\BaseList;
use KupShop\AdminBundle\Util\AdminClassLocator;
use KupShop\AdminBundle\Util\ProductListFilter;
use KupShop\KupShopBundle\Util\Compat\ServiceContainer;
use KupShop\KupShopBundle\Util\HtmlBuilder\HTML;
use Query\Operator;
use Query\QueryBuilder;
class StockInMissingList extends BaseList
{
protected $template = 'list/stockInMissing.tpl';
protected $orderParam = [
'sort' => 'Produkt',
'direction' => 'ASC',
];
public function __construct()
{
$this->tableDef = [
'id' => 'p.id',
'class' => 'getRowClass',
'fields' => [
'product' => ['field' => 'ptitle', 'size' => 2, 'render' => 'renderProduct', 'translate' => true, 'params' => 'refresh=noopener'],
'in_store' => ['field' => 'in_store', 'render' => 'renderInStore', 'size' => 0.4, 'translate' => true],
'suppliers_name' => ['field' => '',
'translate' => true,
'render' => 'renderSuppliersName',
'class' => '',
'size' => 1,
],
'suppliers_code' => ['field' => '', 'render' => 'renderSuppliersCode', 'size' => 1, 'translate' => true],
'suppliers_price' => ['field' => '',
'translate' => true,
'render' => 'renderSuppliersPrice',
'class' => 'text-right alignRight',
'size' => 0.6,
],
'suppliers_in_store' => ['field' => '',
'translate' => true,
'render' => 'renderSuppliersInStore',
'class' => 'text-right alignRight',
'size' => 0.4,
],
'suppliers_buy' => ['field' => '',
'translate' => true,
'render' => 'renderSuppliersBuy',
'class' => '',
'size' => 1,
],
'suppliers_ordered' => ['field' => 'pieces_in_order_list',
'translate' => true,
'render' => 'renderPiecesOrdered',
'tooltip' => 'inOrderTooltip',
'size' => 0.4, 'class' => 'left tiny-list-items align-td',
],
'in_store_min' => ['field' => 'in_store_min', 'size' => 0.7, 'visible' => 'N', 'translate' => true],
'interval_sold' => [
'field' => 'sold',
'size' => 0.5,
'visible' => 'N',
'translate' => fn ($key, $translation_section) => sprintf(translate($key, $translation_section), intval(getVal('sellInterval', null, 30))),
],
'interval_store' => [
'field' => 'remaining_predict',
'render' => 'renderRemaining',
'size' => 0.5,
'visible' => 'N',
'translate' => fn ($key, $translation_section) => sprintf(translate($key, $translation_section), intval(getVal('interval', null, 14))),
],
'days_remaining' => [
'field' => 'days',
'render' => 'renderRound',
'visible' => 'N',
'size' => 0.5,
'translate' => true,
],
'lost' => [
'translate' => getVal('search') == 'missing' ? 'lostMoneyNotOrder' : 'lostMoney',
'field' => 'lost',
'render' => 'renderPrice',
'visible' => 'N',
],
'sell_price' => ['field' => 'price', 'render' => 'renderPriceFinal', 'translate' => true, 'class' => 'text-right alignRight', 'visible' => 'N'],
'future_pieces' => ['field' => 'futurePieces', 'size' => 0.4, 'tooltip' => 'towardsTooltip', 'translate' => true, 'visible' => 'N'],
'preorder_pieces' => ['field' => 'preorderPieces', 'size' => 0.4, 'tooltip' => 'towardsTooltip', 'translate' => true, 'visible' => 'N'],
'ean' => ['field' => 'ean', 'size' => 1, 'visible' => 'N', 'translate' => true],
'image' => ['field' => 'id_photo', 'render' => 'renderImage', 'size' => 0.5, 'visible' => 'N', 'translate' => true],
'margin' => ['field' => '', 'translate' => true, 'visible' => 'N', 'render' => 'renderMargin'],
],
];
}
protected $tableDef = [];
public function customizeTableDef($tableDef)
{
$tableDef = parent::customizeTableDef($tableDef);
if (!findModule(\Modules::ORDERS_OF_SUPPLIERS)) {
unset($tableDef['fields']['suppliers_ordered']);
$tableDef['fields']['suppliers_buy']['size'] = 0.5;
}
$tableDef['fields']['image']['spec'] = function (QueryBuilder $qb) {
$qb->addSelect('ppr.id_photo as id_image, ph.image_2, ph.source as image_source')
->leftJoin('sq', 'photos_products_relation', 'ppr', 'ppr.id_product = sq.id AND ppr.show_in_lead = "Y"')
->leftJoin('ppr', 'photos', 'ph', 'ph.id = ppr.id_photo');
};
$tableDef['fields']['sell_price']['spec'] = function (QueryBuilder $qb) {
$qb->addSelect('sq.price, sq.vat, sq.discount');
};
if (findModule(\Modules::PRODUCTS, \Modules::SUB_NOTE)) {
$tableDef['fields']['product_note'] = [
'field' => 'pnote',
'translate' => true,
'visible' => 'N',
];
}
if (findModule(\Modules::PRODUCTS_BATCHES)) {
$tableDef['fields']['batch'] = ['field' => 'batch', 'size' => 0.5, 'render' => 'renderBoolean', 'visible' => 'N', 'translate' => true];
}
return $tableDef;
}
public function getRowClass($values)
{
$class = 'no-row-click';
if (0 <= $values['pieces_with_orders']) {
$class .= ' row-green';
} elseif ($values['in_store'] < 0) {
$class .= ' row-orange';
}
return $class;
}
public function renderRound($values, $column)
{
$lost = $this->getListRowValue($values, $column['field']);
if (!isset($lost)) {
return '-';
}
return round($lost, 1);
}
public function renderRemaining($values, $column)
{
$lost = $this->getListRowValue($values, $column['field']);
$futurePieces = $values['futurePieces'] ?? 0;
return round($lost + $futurePieces, 1);
}
public function renderInStore($values, $column)
{
$return = HTML::create('p')
->tag('span')
->class('badge')
->text($values['in_store'])
->end();
if (!empty($values['futurePieces'])) {
$return->tag('span')
->class('badge badge-warning')
->attr('title', 'V budoucí faktuře')
->text($values['futurePieces'])
->end();
}
if (!empty($values['preorderPieces'])) {
$return->tag('span')
->class('badge badge-danger')
->attr('title', 'V předobjednávkové faktuře')
->text($values['preorderPieces'])
->end();
}
return $return;
}
protected function renderMargin(array $values, array $column)
{
return $this->renderPerSupplierColumn($values, function ($output, $data) {
$output->text(toDecimal($data['margin'])->round()->asFloat().'%');
});
}
public function renderImage($values, $column)
{
$img = getImage($values['id_image'], $values['image_2'], $values['image_source'], 0);
if (empty($img)) {
$path = '/admin/static/images/no-image.png';
} else {
$path = $this->getUrlFinder()->staticUrl($img['source']);
}
return HTML::create('a')
->attr('href', $path)
->attr('target', '_blank')
->class('btn btn-xs btn-secondary')
->tag('i')
->class('bi bi-zoom-in')
->end()
->end();
}
protected function renderPerSupplierColumn(array $values, callable $callback)
{
$suppliers = $this->getProductSuppliers($values['id'], $values['id_variation']);
$output = HTML::create('div')->tag('table')->class('table-orders-of-suppliers');
foreach ($suppliers as $i => $supplier) {
$td = $output->tag('tr')
->tag('td');
$callback($td, $supplier, $i);
$output->end()
->end();
}
return $output->end();
}
protected function getProductSuppliers(int $id_product, ?int $id_variation): array
{
$id = $id_product.'/'.$id_variation;
if (!isset($this->suppliers[$id])) {
$qb = sqlQueryBuilder()->select('pos.id pos_id, s.name, pos.in_store, s.id as idSupp, pos.code, s.order_url, p.vat')
->from('suppliers', 's')
->leftJoin('s', 'products_of_suppliers', 'pos', 'pos.id_supplier = s.id')
->leftJoin('pos', 'products', 'p', 'p.id = pos.id_product')
->leftJoin('p', 'products_variations', 'pv', 'p.id = pv.id_product')
->andWhere(Operator::andX(
Operator::equals(['pos.id_product' => $id_product]),
Operator::equalsNullable(['pos.id_variation' => $id_variation]),
))
->orderBy('s.id, pos.id_product, pos.id_variation')
->groupBy('s.id');
if (findModule(\Modules::ORDERS_OF_SUPPLIERS)) {
$qb->leftJoin('pos', 'orders_of_suppliers', 'oos',
'pos.id_product = oos.id_product AND pos.id_supplier = oos.id_supplier AND
pos.id_variation<=>oos.id_variation')
->addSelect('COALESCE(oos.pieces, 0) pieces_in_order');
} else {
$qb->addSelect('0 pieces_in_order');
}
$subPriceModule = findModule(\Modules::PRODUCTS, \Modules::SUB_PRICE_BUY);
$qb->addSelect(Operator::coalesce(
'pos.price_buy',
$subPriceModule && findModule(\Modules::PRODUCTS_VARIATIONS) ? 'pv.price_buy' : null,
$subPriceModule ? 'p.price_buy' : null,
0
).' price_buy');
$coalesce = '0';
if ($subPriceModule) {
$coalesce = findModule(\Modules::PRODUCTS_VARIATIONS) ? 'pv.price_buy, p.price_buy' : 'p.price_buy';
}
$qb->addSelect('100 - ((COALESCE(pos.price_buy, '.$coalesce.', 0) / COALESCE(pv.price, p.price)) * 100) as margin');
$this->suppliers[$id] = $qb->execute()->fetchAll();
}
return $this->suppliers[$id];
}
public function renderSuppliersName($values, $column)
{
$suppliers = $this->getProductSuppliers($values['id'], $values['id_variation']);
if (empty($suppliers)) {
return HTML::create('a')
->attr('title', 'Přidat dodavatele')
->attr('href', "javascript:nw('productsOfSuppliers', '0', 'data[id_product]={$values['id']}&data[id_variation]={$values['id_variation']}&autoclose=1');")
->tag('i')
->class('bi bi-plus-lg list-yes')
->end()
->end();
}
return $this->renderPerSupplierColumn($values, function ($output, $data) {
$output->tag('a')
->attr('href', "javascript:nw('productsOfSuppliers', {$data['pos_id']})")
->text($data['name'])
->attr('title', $data['name'])
->end();
});
}
public function renderSuppliersInStore($values, $column)
{
$pieces_ordered = explode(',', $values['pieces_in_order_list']);
return $this->renderPerSupplierColumn($values, function ($output, $data, $i) use ($pieces_ordered) {
if (!is_null($data['in_store'])) {
$supplierAmount = max(($data['in_store'] ?? 0) - $pieces_ordered[$i], 0);
$output->text($supplierAmount);
}
});
}
public function renderSuppliersBuy($values, $column)
{
$search = getVal('search');
$orderAmount = max(0, -(($search == 'missing') ? $values['pieces_with_orders_predict'] : $values['pieces_with_orders']));
return $this->renderPerSupplierColumn($values, function ($output, $data) use ($values, $orderAmount) {
if (!is_null($data['in_store'])) {
$orderAmount = min(ceil($orderAmount), $data['in_store']);
}
if (findModule(\Modules::ORDERS_OF_SUPPLIERS)) {
$output->attr('style', 'width50%;')
->tag('input')
->attr('type', 'number')
->attr('name', 'pieces')
->attr('placeholder', $orderAmount < 0 ? 0 : ceil($orderAmount))
->attr('style', 'width:40px;')
->end()
->text(' ks')
->end()
->tag('td')
->attr('style', 'width:50%;')
->tag('a')
->attr('data-acn', 'OrdersOfSupplierAdd')
->attr('data-adddata', json_encode([
'id_product' => $values['id'],
'id_variation' => $values['id_variation'],
'id_supplier' => $data['idSupp'],
]))
->class('btn btn-xs btn-primary btn-block')
->tag('span')
->class('bi bi-cart')
->end()
->end();
} else {
$output->tag('a')
->attr('data-acn', 'OrdersAdd')
->attr('data-adddata', json_encode([
'code' => $data['code'],
'order_url' => $data['order_url'],
'id_supplier' => $data['idSupp'],
]))
->attr($data['order_url'] ? '' : 'disabled')
->class('btn btn-xs btn-primary btn-block')
->tag('span')
->class('bi bi-cart')
->end()
->text(" {$orderAmount} ks")
->end();
}
});
}
public function renderSuppliersPrice($values, $column)
{
return $this->renderPerSupplierColumn($values, fn ($output, $data) => $output->text(is_null($data['price_buy']) ? '-' :
toDecimal($data['price_buy'])->addVat(getVat($data['vat']))->round()->asFloat().' Kč'));
}
public function renderSuppliersCode($values, $column)
{
return $this->renderPerSupplierColumn($values, fn ($tag, $data) => $tag->text($data['code']));
}
public function renderProduct($values, $column)
{
$html = HTML::create('div')
->class('product-title')
->tag('strong')
->text($values['ptitle'])
->end();
if (!empty($values['pvtitle'])) {
$html->tag('span')
->class('help-block')
->text($values['pvtitle'])
->end();
}
return $html->end();
}
public function renderPiecesOrdered($values, $column)
{
$values = $this->getListRowValue($values, $column['field']);
$output = HTML::create('div')->tag('table')->class('table-orders-of-suppliers');
foreach (explode(',', $values) as $pieces_ordered) {
$output->tag('tr')
->tag('td')
->text($pieces_ordered)
->end()
->end();
}
return $output->end();
}
public function getQuery()
{
/** @var \KupShop\ElninoBundle\\Query\QueryBuilder $qb */
$sellIntervalOriginal = getVal('sellInterval', null, 30);
$sellInterval = intval($sellIntervalOriginal);
$interval = intval(getVal('interval', null, 14));
$search = getVal('search');
$qb = sqlQueryBuilder()
->select(
'p.id',
'pv.id id_variation',
'p.title ptitle',
'pv.title pvtitle',
'p.price pprice',
'pv.price pvprice',
'pos.id_supplier',
'COALESCE(pv.in_store, p.in_store) in_store',
'(COALESCE(pv.in_store, p.in_store, 0) - COALESCE(pv.in_store_min, p.in_store_min, 0)) as in_store_sub_min',
'COALESCE(pv.in_store_min, p.in_store_min) as in_store_min',
'COALESCE(pv.ean, p.ean) as ean',
'COALESCE(pv.price, p.price) as price',
'p.discount',
'p.vat',
'p.producer',
'pos.in_store in_store_supplier',
'pos.code code_supplier',
)
->from('products', 'p')
->leftJoin('p', 'products_variations', 'pv', 'p.id = pv.id_product')
->leftJoin('p', 'products_of_suppliers', 'pos',
'p.id = pos.id_product AND pv.id<=>pos.id_variation')
->groupBy('p.id, pv.id, pos.id_supplier')
->setParameter('interval', $interval)
->setParameter('sellInterval', $sellInterval);
if (findModule(\Modules::SUB_NOTE)) {
$qb->addSelect('COALESCE(pv.note, p.note) as pnote');
}
if (findModule(\Modules::ORDERS_OF_SUPPLIERS)) {
$qb->leftJoin('pos', 'orders_of_suppliers', 'oos',
'pos.id_product = oos.id_product AND pos.id_supplier = oos.id_supplier AND
pos.id_variation<=>oos.id_variation')
->addSelect('COALESCE(oos.pieces, 0) pieces_in_order');
} else {
$qb->addSelect('0 pieces_in_order');
}
$postData = getVal('data', $_POST);
if (!empty($postData['id_product'])) {
$this->addOrdersOfSupplier($postData);
$qb->andWhere(Operator::equalsNullable(['p.id' => $postData['id_product'], 'pv.id' => $postData['id_variation'] ?: null]));
return $this->applyTopQuery($qb);
}
$notHandled = getVal('notHandled', null, false);
$active = getVal('active', null, getVal('fromNav'));
if ($notHandled) {
$existsNotHandled = sqlQueryBuilder()->select('oSub1.id')
->from('order_items', 'oiSub1')
->leftJoin('oiSub1', 'orders', 'oSub1', 'oSub1.id = oiSub1.id_order')
->andWhere('oiSub1.id_product = p.id AND oiSub1.id_variation <=> pv.id')
->andWhere(Operator::inIntArray(getStatuses('nothandled'), 'oSub1.status'))
->groupBy('oiSub1.id_product, oiSub1.id_variation');
$qb->andWhere('EXISTS ('.$existsNotHandled->getSQL().')')
->addParameters($existsNotHandled->getParameters(), $existsNotHandled->getParameterTypes());
}
$ignoreMin = getVal('ignoreMin');
if (!($search == 'iddle' || $search == 'missing') && !$ignoreMin) {
$qb->andWhere('COALESCE(pv.in_store, p.in_store) < COALESCE(pv.in_store_min, p.in_store_min, 0)');
}
if ($active) {
$qb->andWhere('p.figure="Y"');
}
if (findModule(\Modules::PRODUCTS_BATCHES)) {
$qb->leftJoin('p', 'products_batches', 'pb', 'pb.id_product = p.id AND pb.id_variation <=> pv.id')
->addSelect('count(pb.code) > 0 as batch');
}
// Apply product filter
$filter = getVal('filter', null, []);
if (!empty($filter)) {
// extended search
ServiceContainer::getService(ProductListFilter::class)->applyFilter($filter, $qb);
}
return $this->applyTopQuery($qb);
}
public function applyTopQuery($oldqb)
{
$futureSoldAmount = '((COALESCE(pieces_sold.sold, 0) / :sellInterval) * :interval)';
$qb = sqlQueryBuilder()
->select(
'sq.*',
"(COALESCE(sq.in_store_sub_min, 0) - {$futureSoldAmount}) as remaining_predict",
'sq.in_store_sub_min / (COALESCE(pieces_sold.sold, 0) / :sellInterval) as days',
"ROUND(((LEAST(0, sq.in_store_sub_min) - (sq.in_store_sub_min - {$futureSoldAmount})) * COALESCE(sq.pvprice, sq.pprice)), 4) as lost",
'(sq.in_store_sub_min + SUM(COALESCE(sq.pieces_in_order, 0)) + COALESCE(sfp.quantity, 0)) as pieces_with_orders',
"(sq.in_store_sub_min + SUM(COALESCE(sq.pieces_in_order, 0)) + COALESCE(sfp.quantity, 0) - {$futureSoldAmount}) as pieces_with_orders_predict",
'GROUP_CONCAT(COALESCE(sq.pieces_in_order, 0)) pieces_in_order_list',
'SUM(COALESCE(sq.pieces_in_order, 0)) pieces_in_orders',
'SUM(COALESCE(sq.in_store_supplier, 0)) in_store_suppliers',
)
->from('('.$oldqb->getSQL().')', 'sq')
->groupBy('sq.id, sq.id_variation')
->addParameters($oldqb->getParameters(), $oldqb->getParameterTypes());
$subQuerySold = sqlQueryBuilder()->select('oi.id_product, oi.id_variation, SUM(oi.pieces) sold')
->from('orders', 'o')
->innerJoin('o', 'order_items', 'oi', 'o.id = oi.id_order')
->setForceIndexForJoin('oi', 'id_invoice')
->where('o.date_created > (NOW() - INTERVAL :sellInterval DAY) AND o.status_storno = 0')
->groupBy('oi.id_product, oi.id_variation');
$qb->leftJoinSubQuery('sq',
$subQuerySold,
'pieces_sold',
'(sq.id = pieces_sold.id_product AND sq.id_variation <=> pieces_sold.id_variation)');
$qb->addSelect('COALESCE(pieces_sold.sold, 0) sold');
// subquery Na cestě
$subqueryFuturePieces = sqlQueryBuilder()->select('sii.id_product, sii.id_variation, IFNULL(SUM(sii.quantity), 0) quantity')
->from('stock_in', 'si')
->leftJoin('si', 'stock_in_items', 'sii', 'sii.id_stock_in = si.id')
->where("si.id_index = 'future'")
->groupBy('sii.id_product, sii.id_variation');
$qb->leftJoinSubQuery('sq', $subqueryFuturePieces, 'sfp', '(sq.id = sfp.id_product AND sq.id_variation <=> sfp.id_variation)');
// subquery Předobjednávka
$subqueryPreordersPieces = sqlQueryBuilder()->select('sii.id_product, sii.id_variation, IFNULL(SUM(sii.quantity), 0) quantity')
->from('stock_in', 'si')
->leftJoin('si', 'stock_in_items', 'sii', 'sii.id_stock_in = si.id')
->where("si.id_index = 'preorder'")
->groupBy('sii.id_product, sii.id_variation');
$qb->leftJoinSubQuery('sq', $subqueryPreordersPieces, 'spp', '(sq.id = spp.id_product AND sq.id_variation <=> spp.id_variation)');
$qb->addSelect('COALESCE(sfp.quantity,0) futurePieces', 'COALESCE(spp.quantity,0) preorderPieces');
// pokud posílám konkrétní IDčka, nechci už dál filtrovat
$postData = getVal('data', $_POST);
if (!empty($postData['id_product'])) {
return $qb;
}
$search = getVal('search');
$notOrdered = getVal('notOrdered', null, getVal('fromNav'));
$buyable = getVal('buyable');
$ignoreMin = getVal('ignoreMin');
if ($notOrdered && !$ignoreMin) {
if ($search == 'missing') {
$qb->having('pieces_with_orders_predict < 0');
} else {
$qb->having('pieces_with_orders < 0');
}
}
$wasSold = getVal('wasSold', null, getVal('fromNav'));
if ($search == 'missing' && $wasSold) {
$qb->andWhere('COALESCE(pieces_sold.sold, 0) > 0');
}
if ($search == 'missing' && empty(getVal('orderFlags')) && !$ignoreMin) {
$qb->andHaving('remaining_predict + futurePieces <= 0');
} elseif ($search == 'iddle') {
$qb->andHaving('remaining_predict > 0');
}
if ($buyable) {
$qb->andHaving('in_store_suppliers > 0');
}
return $qb;
}
public function getFinalQueryBuilder(array &$vars): QueryBuilder
{
$qb = parent::getFinalQueryBuilder($vars);
$qb->addOrderBy('sq.id_supplier');
return $qb;
}
public function addOrdersOfSupplier($data)
{
if (!empty($data['pieces']) && findModule(\Modules::ORDERS_OF_SUPPLIERS)) {
$adminClassLocator = ServiceContainer::getService(AdminClassLocator::class);
$fullClassPath = $adminClassLocator->getClassPath('ordersOfSuppliers.php');
$orders = $adminClassLocator->createClass($fullClassPath);
$orders->setID($data['id_supplier']);
$orders->handleAddValue($data);
}
}
}
return StockInMissingList::class;

View File

@@ -0,0 +1,69 @@
<?php
namespace KupShop\CatalogBundle\Admin;
use KupShop\KupShopBundle\Util\StringUtil;
use Query\Operator;
class ParameterValues extends \Window
{
protected $tableName = 'parameters_list';
protected $nameField = 'value';
public function get_vars()
{
$vars = parent::get_vars();
$parameterId = $this->getAction() === 'add' ? getVal('parameterId') : getVal('id_parameter', $vars['body']['data']);
$vars['body']['data']['id_parameter'] = $parameterId;
$vars['body']['data']['value_meaning'] = sqlQueryBuilder()
->select('value_meaning')
->from('parameters')
->where(Operator::equals(['id' => $parameterId]))
->execute()->fetchOne();
$this->unserializeCustomData($vars['body']['data']);
return $vars;
}
public function getData()
{
$data = parent::getData();
$colors = [];
// kvuli ukladani dat z adminu - index 0 se v administraci nezobrazuje, proto umele navysim index, aby se barva zobrazila
$index = 1;
foreach ($data['data']['colors'] as $i => $item) {
if ($i === 0 || $item['delete']) {
continue;
}
$colors[$index++] = $item['value'];
}
$data['data']['colors'] = $colors;
if (($data['position'] ?? null) === '') {
unset($data['position']);
}
$this->serializeCustomData($data);
return $data;
}
public function processFormData()
{
$data = parent::processFormData();
if (($data['filter_url'] ?? false) === '') {
$data['filter_url'] = StringUtil::slugify($data['value'] ?? '');
}
return $data;
}
}
return ParameterValues::class;

View File

@@ -0,0 +1,15 @@
<?php
class ProductsPhotos extends BaseAdminPhotos
{
use DatabaseCommunication;
protected $relation_table_name = 'photos_products_descr_plus_relation';
protected $photo_type = 4;
protected $tablefield = 'id_product';
protected $photo_nametype = 'ProductDescrPlus';
protected $copy_from = 'z produktu';
protected $search_field = 'product_id';
}
return ProductsPhotos::class;

View File

@@ -0,0 +1,178 @@
{extends "[shared]window.tpl"}
{block tabs}
{windowTab id='flapParameterValue'}
{/block}
{block tabsContent}
<div id="flapParameterValue" class="tab-pane fade active in boxStatic">
<div class="wpj-main-panel-title">
<h4>Hodnota parametru</h4>
</div>
<input type="hidden" name="data[id_parameter]" value="{$body.data.id_parameter}">
<div class="row">
<div class="col-xs-10">
<div class="wpj-form-group">
<label>{'value'|translate}</label>
<input type="text" class="form-control input-sm" name="data[value]"
value="{$body.data.value}" required>
</div>
</div>
<div class="col-xs-2">
<div class="wpj-form-group">
<label>{'position'|translate}</label>
<input type="text" class="form-control input-sm" name="data[position]"
value="{$body.data.position}">
</div>
</div>
</div>
{block "descriptionInput"}
<div class="row">
{if $body.data.value_meaning == 'progress'}
<div class="col-xs-2">
<div class="wpj-form-group">
<label>
{$body.data.value_meaning|translate:"parameters"}
<a class="help-tip" data-toggle="tooltip"
title="{"progress_tooltip"|translate:"parameters"}">
<i class="bi bi-question-circle"></i>
</a>
</label>
<div class="input-group">
<input type="text" name="data[data][progress][value]" value="{$body.data.data.progress.value}" maxlength="250" class="form-control">
<span class="input-group-addon input-group-addon-light">z</span>
<input type="text" name="data[data][progress][max]" value="{$body.data.data.progress.max}" maxlength="250" class="form-control">
</div>
</div>
</div>
{/if}
<div class="col-xs-{if $body.data.value_meaning == 'progress'}10{else}12{/if}">
<div class="wpj-form-group">
<label>
{if $body.data.value_meaning == 'text' || $body.data.value_meaning == 'progress'}
{'description'|translate}
{else}
{if $body.data.value_meaning == 'color'}
<div class="wpj-form-group d-flex align-items-center">
{print_toggle nameRaw="data[data][enable_multicolor]" value=$body.data.data.enable_multicolor}
<label>{'enable_multicolor'|translate:"parameters"}</label>
</div>
{/if}
{$body.data.value_meaning|translate:"parameters"}
{/if}
</label>
{if $body.data.value_meaning == 'image'}
<div class="input-group">
<input type="text" name="data[description]" value="{$body.data.description}"
maxlength="250"
class="form-control {if $body.data.value_meaning == 'color'}minicolors" autocomplete="off"
data-control="hue{/if}">
{if $body.data.value_meaning == 'image'}
<div class="input-group-btn">
{insert_file_browse link="attachLink"}
</div>
{/if}
</div>
{elseif $body.data.value_meaning == 'color'}
<div id="input-container">
{if !isset($body.data.data.enable_multicolor) or $body.data.data.enable_multicolor === 'N'}
<div class="form-group form-group-flex">
<div class="col-xs-12">
<input type="text" name="data[description]" value="{$body.data.description}" maxlength="250"
class="form-control">
</div>
</div>
{else}
<div id="colors">
<div class="row bottom-space">
<div class="col-md-3">
<a href="#" data-form-add class="btn btn-success btn-block"><span
class="glyphicon glyphicon-plus"></span>&nbsp;{'add_new_color'|translate:'parameters'}</a>
</div>
</div>
<div class="panel-group panel-group-lists">
{if !isset($body.data.data.colors)}
{$body.data.data.colors = []}
{/if}
{foreach [[]] + $body.data.data.colors as $key => $row}
<div class="panel" {if $key == 0}data-form-new style="display:none" {else}data-form-item{/if}>
<div class="row bottom-space">
<div class="col-md-8">
<input type="text" name="data[data][colors][{$key}][value]" {if $key == 0} value="" {else} value="{$row}" {/if} maxlength="250"
class="form-control">
</div>
<div class="col-md-2">
<a class="btn-sm btn btn-danger" data-form-delete>
<input class="hidden" type="checkbox" name="data[data][colors][{$key}][delete]"/>
<span class="glyphicon glyphicon-remove"></span>
</a>
</div>
</div>
</div>
{/foreach}
</div>
</div>
<script type="application/javascript">
initForm({
selector: '#colors',
});
</script>
{/if}
</div>
{else}
<div>
<input type="text" name="data[description]" value="{$body.data.description}" maxlength="250" class="form-control">
</div>
{/if}
</div>
</div>
</div>
{/block}
{ifmodule INDEXED_FILTER}
<div class="row">
<div class="col-xs-6">
<div class="wpj-form-group">
<label>{'filterUrl'|translate}</label>
<input type="text" class="form-control input-sm" name="data[filter_url]"
value="{$body.data.filter_url}">
</div>
</div>
<div class="col-xs-6">
<div class="wpj-form-group">
<label>{'title'|translate}</label>
<input type="text" class="form-control input-sm" name="data[title]"
value="{$body.data.title}">
</div>
</div>
</div>
{/ifmodule}
{block "custom-data"}
{/block}
</div>
{/block}
{block css append}
{if $body.data.value_meaning == 'color'}
<link rel="stylesheet" href="./static/js/jquery.minicolors.css">
{/if}
{/block}
{block js append}
{if $body.data.value_meaning == 'color'}
<script type="text/javascript" src="./static/js/jquery.minicolors.js"></script>
{/if}
{/block}
{block "js-onready" append}
{if $body.data.value_meaning == 'color'}
<script>
$('.minicolors').minicolors();
</script>
{/if}
{/block}

View File

@@ -0,0 +1,116 @@
<div id="flapGPSR" class="tab-pane fade active in boxFlex">
<h4>{'producer'|translate:"producersGPSR"}</h4>
<div class="row">
<div class="col-xs-6">
<div class="wpj-form-group">
<label>{'company_name'|translate:"producersGPSR"}</label>
<input type="text" class="form-control input-sm" maxlength="250" name="data[company_name]"
value="{$body.data.company_name}">
</div>
</div>
</div>
<div class="row">
<div class="col-xs-3">
<div class="wpj-form-group">
<label>{'company_street'|translate:"producersGPSR"}</label>
<input type="text" class="form-control input-sm" maxlength="100" name="data[company_street]"
value="{$body.data.company_street}">
</div>
</div>
<div class="col-xs-3">
<div class="wpj-form-group">
<label>{'company_city'|translate:"producersGPSR"}</label>
<input type="text" class="form-control input-sm" maxlength="50" name="data[company_city]"
value="{$body.data.company_city}">
</div>
</div>
<div class="col-xs-3">
<div class="wpj-form-group">
<label>{'company_zip'|translate:"producersGPSR"}</label>
<input type="text" class="form-control input-sm" maxlength="50" name="data[company_zip]"
value="{$body.data.company_zip}">
</div>
</div>
<div class="col-xs-3">
<div class="wpj-form-group">
<label>{'company_country'|translate:"producersGPSR"}</label>
<input type="text" class="form-control input-sm" maxlength="50" name="data[company_country]"
value="{$body.data.company_country}">
</div>
</div>
</div>
<div class="row">
<div class="col-xs-3">
<div class="wpj-form-group">
<label>{'company_email'|translate:"producersGPSR"}</label>
<input type="text" class="form-control input-sm" maxlength="100" name="data[company_email]"
value="{$body.data.company_email}">
</div>
</div>
<div class="col-xs-3">
<div class="wpj-form-group">
<label>{'company_web'|translate:"producersGPSR"}</label>
<input type="text" class="form-control input-sm" maxlength="100" name="data[company_web]"
value="{$body.data.company_web}">
</div>
</div>
</div>
<h4>{'importer'|translate:"producersGPSR"}</h4>
<p>Vyplňte, pokud se výrobce nenachází v EU</p>
<div class="row">
<div class="col-xs-6">
<div class="wpj-form-group">
<label>{'company_name'|translate:"producersGPSR"}</label>
<input type="text" class="form-control input-sm" maxlength="250" name="data[import_company_name]"
value="{$body.data.import_company_name}">
</div>
</div>
</div>
<div class="row">
<div class="col-xs-3">
<div class="wpj-form-group">
<label>{'company_street'|translate:"producersGPSR"}</label>
<input type="text" class="form-control input-sm" maxlength="100" name="data[import_company_street]"
value="{$body.data.import_company_street}">
</div>
</div>
<div class="col-xs-3">
<div class="wpj-form-group">
<label>{'company_city'|translate:"producersGPSR"}</label>
<input type="text" class="form-control input-sm" maxlength="50" name="data[import_company_city]"
value="{$body.data.import_company_city}">
</div>
</div>
<div class="col-xs-3">
<div class="wpj-form-group">
<label>{'company_zip'|translate:"producersGPSR"}</label>
<input type="text" class="form-control input-sm" maxlength="50" name="data[import_company_zip]"
value="{$body.data.import_company_zip}">
</div>
</div>
<div class="col-xs-3">
<div class="wpj-form-group">
<label>{'company_country'|translate:"producersGPSR"}</label>
<input type="text" class="form-control input-sm" maxlength="50" name="data[import_company_country]"
value="{$body.data.import_company_country}">
</div>
</div>
</div>
<div class="row">
<div class="col-xs-3">
<div class="wpj-form-group">
<label>{'company_email'|translate:"producersGPSR"}</label>
<input type="text" class="form-control input-sm" maxlength="100" name="data[import_company_email]"
value="{$body.data.import_company_email}">
</div>
</div>
<div class="col-xs-3">
<div class="wpj-form-group">
<label>{'company_web'|translate:"producersGPSR"}</label>
<input type="text" class="form-control input-sm" maxlength="100" name="data[import_company_web]"
value="{$body.data.import_company_web}">
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,3 @@
<div id="flapDescriptionPlusPhotos" class="tab-pane fade boxFlex box iframe-box">
<iframe class="on-demand boxFlex" src="launch.php?s=products.descr_plus.photos.php&amp;ID={$body.data.id}" data-src="launch.php?s=products.descr_plus.photos.php&amp;ID={$body.data.id}"></iframe>
</div>

View File

@@ -0,0 +1,29 @@
<script type="text/javascript" src="../ckeditor/ckeditor.js"></script>
<script type="text/javascript" src="static/js/jquery.mjs.nestedSortable.min.js"></script>
<script src="static/js/chosen.image.js"></script>
<script type="text/javascript" src="static/js/blocks.js?2"></script>
<div id="flapBlocks" class="tab-pane boxStatic box">
<h1 class="h4 main-panel-title">
{'flapBlocks'|translate}
</h1>
<div class="row">
<div class="col-xs-6 text-right">
<input type="text" data-no-change data-autocomplete-search="products" autocomplete="off" class="form-control input-sm autocomplete-control" name="copyDescr" placeholder="{'copyDescrPlus'|translate}">
</div>
</div>
{include "utils/blocks.tpl" identifiers=$cfg.Blocks.identifiers blocks=$tab.data.blocks photos=$tab.data.photos acn=$body.acn hideImgBlock=1}
{include "utils/blocksHistory.tpl" blocks_history=$tab.data.blocks_history}
<script>
$('[data-autocomplete-search=products]').adminAutoComplete({
select: function (e, $item) {
var item = $item.data.items[$item.item.data('autocomplete-item')];
window.location = window.location + '&acn=copyDescription&copy_id=' + item.id;
return false;
}
});
</script>
</div>

View File

@@ -0,0 +1,108 @@
{extends 'frame.tpl'}
{block css append}
<style>
.products-filter-list {
display: flex;
flex-wrap: wrap;
}
.autocomplete-results img {
width: 80px;
}
.panel-primary {
margin: 10px;
}
</style>
{/block}
{block windowContent}
<form data-productsFilter-form>
{include "block.productsFilter.tpl"}
</form>
<div class="panel-body" data-productsFilter-preview>
<div class="row">
<div class="col-md-5">
<p><span data-productsFilter-count></span> vyfiltrovaných produktů</p>
</div>
<div class="col-md-5 pull-right">
<button data-productsFilter-button class="btn btn-primary btn-block" onclick="transferValues(filterValue)">Vybrat produkty</button>
</div>
</div>
<br>
<div class="products-filter-list row" data-productsFilter-list></div>
</div>
{/block}
<script>
{block onready append}
'use strict';
function transferValues(val) {
var opener = window.opener;
if (opener) {
opener.postMessage({ selectedProducts: 'selectedProducts', filter: val }, '*');
opener.focus();
window.close();
}
}
var filterValue = '{$smarty.get.filter|escape:javascript nofilter}';
$(document).ready(function() {
var $form = $('[data-productsFilter-form]');
var $preview = $('[data-productsFilter-preview]');
var $count = $('[data-productsFilter-count]');
var $button = $('[data-productsFilter-button]');
var $products = $('[data-productsFilter-list]');
var createProductMarkup = function(data) {
return $('<div class="col-lg-3 col-md-4 col-xs-6"><div class="panel autocomplete-results"><div class="panel-body"><img src="' + data.photo +
'" alt=""><p class="product-title"><a href="javascript:nw(\'product\', \'' + data.id + '\')">' + data.title + '</a><br><small>Kód: ' + data.code + ' | Skladem: ' +
data.inStore + ' ks</small></p></div></div></div>');
};
var xhr = null;
function loadProducts() {
$button.addClass('is-submitting');
resetTimer('productsFiler', 1000, function() {
// preview
if (xhr) {
xhr.abort();
}
xhr = $.get('launch.php?s=ProductsFilter.php&acn=loadProducts&' + $form.serialize(), function(data) {
$count.text(data.count);
filterValue = data.filter;
$products.html('');
if (data.count > 0) {
$.each(data.products, function(index, product) {
$products.append(createProductMarkup(product));
});
}
$button.removeClass('is-submitting');
$preview.slideDown();
});
});
}
{if $productsSortable}
$form.on('orderChanged', function (event) {
loadProducts();
});
{/if}
$form.on('change input', ':input', function(event) {
// Ignore changes from products autocomplete
if ($(event.target).is('.chosen-search-input')) {
return;
}
event.preventDefault();
loadProducts();
});
loadProducts();
});
{/block}
</script>

View File

@@ -0,0 +1,207 @@
<div id="flapSearch" class="tab-pane boxStatic box">
<div class="row">
<div class="col-xs-12">
<p>{'filtersIntroText'|translate:'sections'}</p>
</div>
</div>
<div class="row m-b-1">
<div class="col-xs-6 d-flex align-items-center">
{if $tab.data.type == 'sections' && $tab.data.ID > 0}
{print_toggle attrs="id='data[inherit]'" nameRaw="data[inherit]" value=$tab.data.inherit}
<label for="data[inherit]">{'filtersInherit'|translate:'sections'}</label>
{/if}
</div>
<div class="col-xs-6 d-flex align-items-center justify-content-end">
{print_toggle attrs="id='hideInactive'" nameRaw="hideInactive" disabled=($tab.data.inherit == "Y")}
<label for="hideInactive">{'filtersHideInactive'|translate:'sections'}</label>
<script type="text/javascript">
$(() => {
function getInactiveItems() {
return $('[data-filter-item-enabler]').filter((i, el) => !$(el).prop('checked')).closest('[data-filter-item]');
}
$('input[name="hideInactive"]').on('change', function () {
getInactiveItems().toggleClass('hidden')
});
});
</script>
</div>
</div>
<div class="wpj-panel wpj-panel-default">
<div class="wpj-panel-heading">
<div class="row">
<div class="col-xs-3">
<span class="drag-drop-mover"><i class="bi bi-arrows-move handle"></i></span>
<small>{'name'|translate:'sections'}</small>
</div>
<div class="col-xs-2">
<small>{'type'|translate:'sections'}</small>
</div>
<div class="col-xs-2">
<small>{'filterEnable'|translate:'sections'}</small>
</div>
<div class="col-xs-3">
</div>
{ifmodule CONVERTORS}
<div class="col-xs-2">
<small>{'filterVariationConvertor'|translate:'sections'}</small>
</div>
{/ifmodule}
</div>
</div>
<div class="wpj-list-group m-b-0" id="sorter" disabled>
{foreach from=$tab.data.filterItems item=filter key=key}
{if $tab.data.inherit == "Y" and $filter.enabled != "Y"}
{continue}
{/if}
{* String parametry nejdou použít jako filtr *}
{if $filter.type == 'parameter' && $filter.value_type == 'char' && $filter.enabled != "Y"}
{continue}
{/if}
{if $filter.type == 'sellers' && $filter.enabled != "Y" && findModule('sellers','my_seller')}
{continue}
{/if}
<div class="wpj-list-group-item{if $tab.data.inherit == "Y" and $filter.enabled == "Y"} disabled{/if}" data-filter-item="{$key}">
<div class="row">
<div class="col-xs-3">
{if $filter.id and ($filter.id_source_section == $tab.data.ID or $filter.id_source_producer == $tab.data.ID)}
<input type="hidden" name="data[filters][{$key}][id]" value="{$filter.id}" />
{/if}
<input type="hidden" name="data[filters][{$key}][type]" value="{$filter.type}" />
<input
type="hidden"
name="data[filters][{$key}][{if $tab.data.type == 'sections'}id_source_section{else}id_source_producer{/if}]"
value="{$tab.data.ID}"
/>
{if $filter.type == 'parameter'}
<input type="hidden" name="data[filters][{$key}][id_parameter]" value="{$filter.id_parameter}" />
{elseif $filter.type == 'variation'}
<input type="hidden" name="data[filters][{$key}][id_variation_label]" value="{$filter.id_variation_label}" />
{/if}
<div class="d-flex align-items-center">
<input type="hidden" name="data[filters][{$key}][position]" value="{$filter.position|default:-1}"
data-sort=""/>
<span class="drag-drop-mover{if $filter.enabled != "Y"} disabled{/if}">
<i class="bi bi-arrows-move handle"></i>
</span>
<strong>
{if !$filter.name && $filter.type}
{"filter_`$filter.type`"|translate:'filters'}
{else}
{$filter.name}
{/if}
</strong>
{if $filter.type == 'parameter' and $filter.value_type == 'char'}
<a class="help-tip" data-toggle="tooltip" title="" data-original-title="Parameter typu &quot;text&quot; není podporován a proto nebude v sekci zobrazen.">
<span class="list-no bi bi-x-circle-fill"></span>
</a>
{/if}
</div>
</div>
<div class="col-xs-2">
{if $filter.type == 'variation'}
<span class="d-flex align-items-center">
<i class="bi bi-lg bi-diagram-2 m-r-2 text-muted"></i>{'typeVariation'|translate:'sections'}
</span>
{elseif $filter.type == 'parameter'}
<span class="d-flex align-items-center">
<i class="bi bi-lg bi-sliders m-r-2 text-muted"></i>{'typeParameter'|translate:'sections'}
</span>
{else}
<span class="d-flex align-items-center">
<i class="bi bi-lg bi-gear m-r-2 text-muted"></i>{'typeOther'|translate:'sections'}
</span>
{/if}
</div>
<div class="col-xs-2">
{print_toggle nameRaw="data[filters][`$key`][enabled]" value=$filter.enabled attrs="data-filter-item-enabler=\"`$key`\"" disabled=($tab.data.inherit == "Y")}
{if $tab.data.inherit == "Y" and $filter.enabled == "Y"}
<input type="hidden" name="data[filters][{$key}][enabled]" value="{$filter.enabled}">
{/if}
<script type="text/javascript">
$(() => {
$('input[name="data[filters][{$key}][enabled]"]').on('change', function () {
$(this).closest('[data-filter-item]').find('.drag-drop-mover').toggleClass('disabled');
});
});
</script>
</div>
<div class="col-xs-3">
{ifmodule INDEXED_FILTER}
{if $filter.indexing_allowed|default:true and ($filter.type == 'parameter' or $filter.type == 'variation' or $filter.type == 'producer')}
<div class="d-flex justify-content-between" style="max-width: 260px;">
<div class="checkbox">
<input type="hidden" name="data[filters][{$key}][indexing]" value="N">
<input type="checkbox" class="check" name="data[filters][{$key}][indexing]" id="PRMINDEX{$key}"
value="Y" {if $filter.indexing == 'Y'}checked="checked"{/if} />
<label for="PRMINDEX{$key}">
{'Index'|translate:'filters'}
</label>
</div>
<div class="checkbox">
<input type="hidden" name="data[filters][{$key}][to_title]" value="N">
<input type="checkbox" class="check" name="data[filters][{$key}][to_title]" id="PRMTITLE{$key}"
value="Y" {if $filter.to_title == 'Y'}checked="checked"{/if} />
<label for="PRMTITLE{$key}">
{'toTitle'|translate:'filters'}
</label>
</div>
</div>
{/if}
{/ifmodule}
</div>
{ifmodule CONVERTORS}
<div class="col-xs-2">
{if $filter.type == 'variation'}
<select
class="selecter"
name="data[filters][{$key}][convertor]" data-type="convertor"
data-preload="Convertors"
data-filter-type="autocomplete"
data-autocomplete="Convertors"
data-autocomplete-params="allow_empty=1"
>
{if $filter.convertor}
<option value="{$filter.convertor}" selected>{$filter.convertor}</option>
{/if}
</select>
{/if}
</div>
{/ifmodule}
</div>
</div>
{/foreach}
</div>
</div>
<script type="text/javascript">
sort_section('#sorter');
function sort_section(name) {
$(name).sortable({
helper: function (e, row) {
var $row = $(row);
var $helper = $row.clone().addClass('drag-drop');
return $helper[0];
},
stop: function (event, ui) {
$(this).children().each(function (index, item) {
$(item).find('[data-sort]').val($(item).index()).change();
});
window.somethingChanged = true;
},
handle: '.handle',
placeholder: 'placeholder',
});
}
</script>
</div>