Files
prestashop-entity-selector/src/EntitySelector/EntitySearchEngine.php
myprestarocks 451a47cdcd Add schedule preview dropdown styles and various improvements
- Add .schedule-preview-dropdown and .schedule-preview-item CSS classes
- Add .btn-schedule-preview badge styling
- Add preview functionality for entity list views
- Improve modal and dropdown styling
- Various JS and SCSS enhancements

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-04 11:49:01 +00:00

3565 lines
122 KiB
PHP

<?php
/**
* Entity Search Engine
*
* Handles AJAX search functionality for all entity types.
* Provides search, count, and getByIds methods for each entity.
*
* @package MyPrestaRocks\EntitySelector
*/
namespace MyPrestaRocks\EntitySelector\EntitySelector;
if (!defined('_PS_VERSION_')) {
exit;
}
use Db;
use DbQuery;
use Context;
use Configuration;
use Tools;
use ImageType;
use Shop;
class EntitySearchEngine
{
/**
* @var int Language ID
*/
protected $idLang;
/**
* @var int Shop ID
*/
protected $idShop;
/**
* Constructor
*
* @param int $idLang Language ID
* @param int $idShop Shop ID
*/
public function __construct($idLang = null, $idShop = null)
{
$this->idLang = $idLang ?: (int) Context::getContext()->language->id;
$this->idShop = $idShop ?: (int) Context::getContext()->shop->id;
}
/**
* Search for entities by type
*
* @param string $entityType Entity type key
* @param string $query Search query
* @param int $limit Results limit
* @param int $offset Results offset
* @param array $filters Additional filters
* @return array Search results
*/
public function search($entityType, $query, $limit = 20, $offset = 0, array $filters = [])
{
$method = 'searchTarget' . $this->camelCase($entityType);
if (method_exists($this, $method)) {
return $this->$method($query, $this->idLang, $this->idShop, $limit, $offset, $filters);
}
return [];
}
/**
* Count entities by type
*
* @param string $entityType Entity type key
* @param string $query Search query
* @param array $filters Additional filters
* @return int Total count
*/
public function count($entityType, $query, array $filters = [])
{
$method = 'countTarget' . $this->camelCase($entityType);
if (method_exists($this, $method)) {
return $this->$method($query, $this->idLang, $this->idShop, $filters);
}
return 0;
}
/**
* Get entities by IDs
*
* @param string $entityType Entity type key
* @param array $ids Entity IDs
* @return array Entities data
*/
public function getByIds($entityType, array $ids)
{
$method = 'getTarget' . $this->camelCase($entityType) . 'ByIds';
if (method_exists($this, $method)) {
return $this->$method($ids, $this->idLang, $this->idShop);
}
return [];
}
/**
* Convert snake_case to CamelCase
*
* @param string $string
* @return string
*/
protected function camelCase($string)
{
return str_replace(' ', '', ucwords(str_replace(['_', '-'], ' ', $string)));
}
/**
* Escape pattern for LIKE queries
*
* @param string $pattern
* @return string
*/
protected function escapePattern($pattern)
{
return str_replace(['%', '_'], ['\\%', '\\_'], pSQL($pattern));
}
/**
* Build ORDER BY clause based on entity type and sort field
*
* @param string $entityType Entity type (products, categories, etc.)
* @param array $filters Filters containing sort_by and sort_dir
* @param array $columnMap Mapping of sort field names to SQL columns
* @return string ORDER BY clause
*/
protected function buildOrderBy($entityType, array $filters, array $columnMap)
{
$sortBy = $filters['sort_by'] ?? 'name';
$sortDir = strtoupper($filters['sort_dir'] ?? 'ASC');
if (!in_array($sortDir, ['ASC', 'DESC'])) {
$sortDir = 'ASC';
}
// Get the column for sorting
$column = $columnMap[$sortBy] ?? $columnMap['name'] ?? 'name';
return $column . ' ' . $sortDir;
}
// =========================================================================
// PRODUCTS
// =========================================================================
/**
* Search products
*
* @param string $query Search query
* @param int $idLang Language ID
* @param int $idShop Shop ID
* @param int $limit Results limit
* @param int $offset Results offset
* @param array $filters Additional filters
* @return array Product results
*/
public function searchTargetProducts($query, $idLang, $idShop, $limit = 20, $offset = 0, array $filters = [])
{
$sql = new DbQuery();
$sql->select('DISTINCT p.id_product, pl.name, p.reference, ps.price AS base_price, p.active');
$sql->select('sa.quantity AS stock_qty, m.name AS manufacturer_name');
$sql->select('cl.name AS category_name, p.id_category_default');
$sql->select('i.id_image');
// Check for discount
$sql->select('(ps.on_sale = 1 OR EXISTS (
SELECT 1 FROM ' . _DB_PREFIX_ . 'specific_price sp
WHERE sp.id_product = p.id_product
AND sp.reduction > 0
AND (sp.from = "0000-00-00 00:00:00" OR sp.from <= NOW())
AND (sp.to = "0000-00-00 00:00:00" OR sp.to >= NOW())
)) AS has_discount');
// Sales quantity
$sql->select('(SELECT COALESCE(SUM(od.product_quantity), 0) FROM ' . _DB_PREFIX_ . 'order_detail od
INNER JOIN ' . _DB_PREFIX_ . 'orders o ON o.id_order = od.id_order
WHERE od.product_id = p.id_product AND o.valid = 1) AS sales_qty');
$sql->from('product', 'p');
$sql->innerJoin('product_shop', 'ps', 'ps.id_product = p.id_product AND ps.id_shop = ' . (int) $idShop);
$sql->leftJoin('product_lang', 'pl', 'pl.id_product = p.id_product AND pl.id_lang = ' . (int) $idLang . ' AND pl.id_shop = ' . (int) $idShop);
$sql->leftJoin('manufacturer', 'm', 'm.id_manufacturer = p.id_manufacturer');
$sql->leftJoin('category_lang', 'cl', 'cl.id_category = p.id_category_default AND cl.id_lang = ' . (int) $idLang . ' AND cl.id_shop = ' . (int) $idShop);
$sql->leftJoin('stock_available', 'sa', 'sa.id_product = p.id_product AND sa.id_product_attribute = 0 AND sa.id_shop = ' . (int) $idShop);
$sql->leftJoin('image', 'i', 'i.id_product = p.id_product AND i.cover = 1');
// Search query
if (!empty($query)) {
$escapedQuery = $this->escapePattern($query);
$sql->where('(pl.name LIKE \'%' . $escapedQuery . '%\' OR p.reference LIKE \'%' . $escapedQuery . '%\' OR p.id_product = ' . (int) $query . ')');
}
// Apply filters
$this->applyProductFilters($sql, $filters, $idLang, $idShop);
// Refine query
if (!empty($filters['refine'])) {
$refine = $this->escapePattern($filters['refine']);
if (!empty($filters['refine_negate'])) {
$sql->where('pl.name NOT LIKE \'%' . $refine . '%\'');
} else {
$sql->where('pl.name LIKE \'%' . $refine . '%\'');
}
}
// Sorting
// Note: Products don't have a global position - only per-category positions in ps_category_product
$productSortMap = [
'name' => 'pl.name',
'id' => 'p.id_product',
'price' => 'ps.price',
'reference' => 'p.reference',
'popularity' => 'sales_qty',
'stock' => 'stock_qty',
];
$sql->orderBy($this->buildOrderBy('products', $filters, $productSortMap));
$sql->limit((int) $limit, (int) $offset);
$results = Db::getInstance()->executeS($sql);
if (!$results) {
return [];
}
$imageType = $this->getProductImageType();
$context = Context::getContext();
$locale = $context->getCurrentLocale();
$currency = $context->currency;
return array_map(function ($row) use ($imageType, $idLang, $idShop, $locale, $currency) {
$basePrice = (float) ($row['base_price'] ?? 0);
$stockQty = (int) ($row['stock_qty'] ?? 0);
$salesQty = (int) ($row['sales_qty'] ?? 0);
$hasDiscount = (bool) ($row['has_discount'] ?? false);
// Format prices
$regularPriceFormatted = $locale->formatPrice($basePrice, $currency->iso_code);
// If has discount, get the actual discounted price
$finalPriceFormatted = $regularPriceFormatted;
if ($hasDiscount) {
$product = new \Product((int) $row['id_product'], false, $idLang, $idShop);
$finalPrice = $product->getPrice(true, null, 2);
$finalPriceFormatted = $locale->formatPrice($finalPrice, $currency->iso_code);
}
// Determine stock status
$stockStatus = 'in_stock';
if ($stockQty <= 0) {
$stockStatus = 'out_of_stock';
} elseif ($stockQty <= 5) {
$stockStatus = 'low_stock';
}
return [
'id' => (int) $row['id_product'],
'type' => 'product',
'name' => $row['name'],
'subtitle' => $row['reference'] ? 'Ref: ' . $row['reference'] : null,
'reference' => $row['reference'],
'active' => (bool) $row['active'],
'manufacturer' => $row['manufacturer_name'],
'category' => $row['category_name'],
'image' => $row['id_image'] ? $this->getProductImageUrl($row['id_product'], $row['id_image'], $imageType) : null,
// Formatted fields for list view columns (matching TargetConditions)
'regular_price_formatted' => $regularPriceFormatted,
'price_formatted' => $finalPriceFormatted,
'has_discount' => $hasDiscount,
'stock_qty' => $stockQty,
'stock_status' => $stockStatus,
'sales_qty' => $salesQty,
];
}, $results);
}
/**
* Count products
*
* @param string $query Search query
* @param int $idLang Language ID
* @param int $idShop Shop ID
* @param array $filters Additional filters
* @return int Total count
*/
public function countTargetProducts($query, $idLang, $idShop, array $filters = [])
{
$sql = new DbQuery();
$sql->select('COUNT(DISTINCT p.id_product)');
$sql->from('product', 'p');
$sql->innerJoin('product_shop', 'ps', 'ps.id_product = p.id_product AND ps.id_shop = ' . (int) $idShop);
$sql->leftJoin('product_lang', 'pl', 'pl.id_product = p.id_product AND pl.id_lang = ' . (int) $idLang . ' AND pl.id_shop = ' . (int) $idShop);
if (!empty($query)) {
$escapedQuery = $this->escapePattern($query);
$sql->where('(pl.name LIKE \'%' . $escapedQuery . '%\' OR p.reference LIKE \'%' . $escapedQuery . '%\' OR p.id_product = ' . (int) $query . ')');
}
$this->applyProductFilters($sql, $filters, $idLang, $idShop);
if (!empty($filters['refine'])) {
$refine = $this->escapePattern($filters['refine']);
if (!empty($filters['refine_negate'])) {
$sql->where('pl.name NOT LIKE \'%' . $refine . '%\'');
} else {
$sql->where('pl.name LIKE \'%' . $refine . '%\'');
}
}
return (int) Db::getInstance()->getValue($sql);
}
/**
* Get products by IDs
*
* @param array $ids Product IDs
* @param int $idLang Language ID
* @param int $idShop Shop ID
* @return array Products data
*/
public function getTargetProductsByIds(array $ids, $idLang, $idShop = null)
{
if (empty($ids)) {
return [];
}
$idShop = $idShop ?: $this->idShop;
$sql = new DbQuery();
$sql->select('p.id_product, pl.name, p.reference, ps.price AS base_price, p.active, i.id_image');
$sql->select('sa.quantity AS stock_qty');
// Check for discount
$sql->select('(ps.on_sale = 1 OR EXISTS (
SELECT 1 FROM ' . _DB_PREFIX_ . 'specific_price sp
WHERE sp.id_product = p.id_product
AND sp.reduction > 0
AND (sp.from = "0000-00-00 00:00:00" OR sp.from <= NOW())
AND (sp.to = "0000-00-00 00:00:00" OR sp.to >= NOW())
)) AS has_discount');
$sql->from('product', 'p');
$sql->innerJoin('product_shop', 'ps', 'ps.id_product = p.id_product AND ps.id_shop = ' . (int) $idShop);
$sql->leftJoin('product_lang', 'pl', 'pl.id_product = p.id_product AND pl.id_lang = ' . (int) $idLang . ' AND pl.id_shop = ' . (int) $idShop);
$sql->leftJoin('image', 'i', 'i.id_product = p.id_product AND i.cover = 1');
$sql->leftJoin('stock_available', 'sa', 'sa.id_product = p.id_product AND sa.id_product_attribute = 0 AND sa.id_shop = ' . (int) $idShop);
$sql->where('p.id_product IN (' . implode(',', array_map('intval', $ids)) . ')');
$results = Db::getInstance()->executeS($sql);
if (!$results) {
return [];
}
$imageType = $this->getProductImageType();
$context = Context::getContext();
$locale = $context->getCurrentLocale();
$currency = $context->currency;
$products = [];
foreach ($results as $row) {
$basePrice = (float) ($row['base_price'] ?? 0);
$stockQty = (int) ($row['stock_qty'] ?? 0);
$hasDiscount = (bool) ($row['has_discount'] ?? false);
// Format prices
$regularPriceFormatted = $locale->formatPrice($basePrice, $currency->iso_code);
// If has discount, get the actual discounted price
$finalPriceFormatted = $regularPriceFormatted;
if ($hasDiscount) {
$product = new \Product((int) $row['id_product'], false, $idLang, $idShop);
$finalPrice = $product->getPrice(true, null, 2);
$finalPriceFormatted = $locale->formatPrice($finalPrice, $currency->iso_code);
}
// Determine stock status
$stockStatus = 'in_stock';
if ($stockQty <= 0) {
$stockStatus = 'out_of_stock';
} elseif ($stockQty <= 5) {
$stockStatus = 'low_stock';
}
$products[(int) $row['id_product']] = [
'id' => (int) $row['id_product'],
'type' => 'product',
'name' => $row['name'],
'subtitle' => $row['reference'] ? 'Ref: ' . $row['reference'] : null,
'reference' => $row['reference'],
'active' => (bool) $row['active'],
'image' => $row['id_image'] ? $this->getProductImageUrl($row['id_product'], $row['id_image'], $imageType) : null,
// Formatted fields for list view columns (matching TargetConditions)
'regular_price_formatted' => $regularPriceFormatted,
'price_formatted' => $finalPriceFormatted,
'has_discount' => $hasDiscount,
'stock_qty' => $stockQty,
'stock_status' => $stockStatus,
];
}
// Return in original order
$ordered = [];
foreach ($ids as $id) {
if (isset($products[(int) $id])) {
$ordered[] = $products[(int) $id];
}
}
return $ordered;
}
/**
* Apply product-specific filters to query
*
* @param DbQuery $sql Query builder
* @param array $filters Filters to apply
* @param int $idLang Language ID
* @param int $idShop Shop ID
*/
protected function applyProductFilters(DbQuery $sql, array $filters, $idLang, $idShop)
{
// Stock filter
if (!empty($filters['in_stock'])) {
$sql->leftJoin('stock_available', 'sa_filter', 'sa_filter.id_product = p.id_product AND sa_filter.id_product_attribute = 0 AND sa_filter.id_shop = ' . (int) $idShop);
$sql->where('sa_filter.quantity > 0');
}
// Discounted filter
if (!empty($filters['discounted'])) {
$sql->innerJoin('specific_price', 'sp', 'sp.id_product = p.id_product');
$sql->where('sp.reduction > 0');
$sql->where('(sp.from = \'0000-00-00 00:00:00\' OR sp.from <= NOW())');
$sql->where('(sp.to = \'0000-00-00 00:00:00\' OR sp.to >= NOW())');
}
// Price range
if ($filters['price_min'] !== null) {
$sql->where('p.price >= ' . (float) $filters['price_min']);
}
if ($filters['price_max'] !== null) {
$sql->where('p.price <= ' . (float) $filters['price_max']);
}
// Active only
if (!empty($filters['active_only'])) {
$sql->where('ps.active = 1');
}
// Attribute filter
if (!empty($filters['attributes'])) {
foreach ($filters['attributes'] as $attrFilter) {
if (!empty($attrFilter['values'])) {
$attrIds = array_map('intval', $attrFilter['values']);
$sql->innerJoin('product_attribute_combination', 'pac_' . (int) $attrFilter['id_attribute_group'], 'pac_' . (int) $attrFilter['id_attribute_group'] . '.id_attribute IN (' . implode(',', $attrIds) . ')');
$sql->innerJoin('product_attribute', 'pa_' . (int) $attrFilter['id_attribute_group'], 'pa_' . (int) $attrFilter['id_attribute_group'] . '.id_product_attribute = pac_' . (int) $attrFilter['id_attribute_group'] . '.id_product_attribute AND pa_' . (int) $attrFilter['id_attribute_group'] . '.id_product = p.id_product');
}
}
}
// Feature filter
if (!empty($filters['features'])) {
foreach ($filters['features'] as $featFilter) {
if (!empty($featFilter['values'])) {
$featIds = array_map('intval', $featFilter['values']);
$sql->innerJoin('feature_product', 'fp_' . (int) $featFilter['id_feature'], 'fp_' . (int) $featFilter['id_feature'] . '.id_product = p.id_product AND fp_' . (int) $featFilter['id_feature'] . '.id_feature_value IN (' . implode(',', $featIds) . ')');
}
}
}
// Date filters
if (!empty($filters['date_add_from'])) {
$sql->where('p.date_add >= \'' . pSQL($filters['date_add_from']) . ' 00:00:00\'');
}
if (!empty($filters['date_add_to'])) {
$sql->where('p.date_add <= \'' . pSQL($filters['date_add_to']) . ' 23:59:59\'');
}
}
/**
* Get product image type for thumbnails
*
* @return string
*/
protected function getProductImageType()
{
$type = ImageType::getFormattedName('small');
return $type ?: 'small_default';
}
/**
* Get product image URL
*
* @param int $idProduct Product ID
* @param int $idImage Image ID
* @param string $type Image type
* @return string
*/
protected function getProductImageUrl($idProduct, $idImage, $type)
{
$link = Context::getContext()->link;
return $link->getImageLink('product', $idProduct . '-' . $idImage, $type);
}
// =========================================================================
// CATEGORIES
// =========================================================================
/**
* Search categories
*
* @param string $query Search query
* @param int $idLang Language ID
* @param int $idShop Shop ID
* @param int $limit Results limit
* @param int $offset Results offset
* @param array $filters Additional filters
* @return array Category results
*/
public function searchTargetCategories($query, $idLang, $idShop, $limit = 20, $offset = 0, array $filters = [])
{
$sql = new DbQuery();
$sql->select('DISTINCT c.id_category, cl.name, c.active, c.level_depth, c.position');
$sql->select('(SELECT COUNT(cp2.id_product) FROM ' . _DB_PREFIX_ . 'category_product cp2 WHERE cp2.id_category = c.id_category) AS product_count');
$sql->from('category', 'c');
$sql->innerJoin('category_shop', 'cs', 'cs.id_category = c.id_category AND cs.id_shop = ' . (int) $idShop);
$sql->leftJoin('category_lang', 'cl', 'cl.id_category = c.id_category AND cl.id_lang = ' . (int) $idLang . ' AND cl.id_shop = ' . (int) $idShop);
// Exclude root category
$sql->where('c.id_parent > 0');
if (!empty($query)) {
$escapedQuery = $this->escapePattern($query);
$sql->where('(cl.name LIKE \'%' . $escapedQuery . '%\' OR c.id_category = ' . (int) $query . ')');
}
// Refine query
if (!empty($filters['refine'])) {
$refine = $this->escapePattern($filters['refine']);
if (!empty($filters['refine_negate'])) {
$sql->where('cl.name NOT LIKE \'%' . $refine . '%\'');
} else {
$sql->where('cl.name LIKE \'%' . $refine . '%\'');
}
}
// Depth filter
if ($filters['depth'] !== null) {
$sql->where('c.level_depth = ' . (int) $filters['depth']);
}
// Product count filter
if ($filters['product_count_min'] !== null || $filters['product_count_max'] !== null) {
$having = [];
if ($filters['product_count_min'] !== null) {
$having[] = 'product_count >= ' . (int) $filters['product_count_min'];
}
if ($filters['product_count_max'] !== null) {
$having[] = 'product_count <= ' . (int) $filters['product_count_max'];
}
// Having clause needs GROUP BY - restructure query
$sql->groupBy('c.id_category');
foreach ($having as $h) {
$sql->having($h);
}
}
// Has products filter
if (!empty($filters['has_products'])) {
$sql->innerJoin('category_product', 'cp_filter', 'cp_filter.id_category = c.id_category');
}
// Active filter
if (!empty($filters['active_only'])) {
$sql->where('c.active = 1');
}
// Sorting
$categorySortMap = [
'name' => 'cl.name',
'id' => 'c.id_category',
'position' => 'c.position',
'product_count' => 'product_count',
];
$sql->orderBy($this->buildOrderBy('categories', $filters, $categorySortMap));
$sql->limit((int) $limit, (int) $offset);
$results = Db::getInstance()->executeS($sql);
if (!$results) {
return [];
}
return array_map(function ($row) {
return [
'id' => (int) $row['id_category'],
'type' => 'category',
'name' => $row['name'],
'active' => (bool) $row['active'],
'level_depth' => (int) $row['level_depth'],
'position' => (int) $row['position'],
'product_count' => (int) $row['product_count'],
'breadcrumb' => $this->getCategoryBreadcrumb((int) $row['id_category']),
];
}, $results);
}
/**
* Count categories
*
* @param string $query Search query
* @param int $idLang Language ID
* @param int $idShop Shop ID
* @param array $filters Additional filters
* @return int Total count
*/
public function countTargetCategories($query, $idLang, $idShop, array $filters = [])
{
$sql = new DbQuery();
$sql->select('COUNT(DISTINCT c.id_category)');
$sql->from('category', 'c');
$sql->innerJoin('category_shop', 'cs', 'cs.id_category = c.id_category AND cs.id_shop = ' . (int) $idShop);
$sql->leftJoin('category_lang', 'cl', 'cl.id_category = c.id_category AND cl.id_lang = ' . (int) $idLang . ' AND cl.id_shop = ' . (int) $idShop);
$sql->where('c.id_parent > 0');
if (!empty($query)) {
$escapedQuery = $this->escapePattern($query);
$sql->where('(cl.name LIKE \'%' . $escapedQuery . '%\' OR c.id_category = ' . (int) $query . ')');
}
if (!empty($filters['refine'])) {
$refine = $this->escapePattern($filters['refine']);
if (!empty($filters['refine_negate'])) {
$sql->where('cl.name NOT LIKE \'%' . $refine . '%\'');
} else {
$sql->where('cl.name LIKE \'%' . $refine . '%\'');
}
}
if ($filters['depth'] !== null) {
$sql->where('c.level_depth = ' . (int) $filters['depth']);
}
if (!empty($filters['has_products'])) {
$sql->innerJoin('category_product', 'cp_filter', 'cp_filter.id_category = c.id_category');
}
if (!empty($filters['active_only'])) {
$sql->where('c.active = 1');
}
return (int) Db::getInstance()->getValue($sql);
}
/**
* Get categories by IDs
*
* @param array $ids Category IDs
* @param int $idLang Language ID
* @param int $idShop Shop ID
* @return array Categories data
*/
public function getTargetCategoriesByIds(array $ids, $idLang, $idShop = null)
{
if (empty($ids)) {
return [];
}
$idShop = $idShop ?: $this->idShop;
$sql = new DbQuery();
$sql->select('c.id_category, cl.name, c.active, c.level_depth');
$sql->from('category', 'c');
$sql->leftJoin('category_lang', 'cl', 'cl.id_category = c.id_category AND cl.id_lang = ' . (int) $idLang . ' AND cl.id_shop = ' . (int) $idShop);
$sql->where('c.id_category IN (' . implode(',', array_map('intval', $ids)) . ')');
$results = Db::getInstance()->executeS($sql);
if (!$results) {
return [];
}
$categories = [];
foreach ($results as $row) {
$categories[(int) $row['id_category']] = [
'id' => (int) $row['id_category'],
'type' => 'category',
'name' => $row['name'],
'active' => (bool) $row['active'],
'breadcrumb' => $this->getCategoryBreadcrumb((int) $row['id_category']),
];
}
$ordered = [];
foreach ($ids as $id) {
if (isset($categories[(int) $id])) {
$ordered[] = $categories[(int) $id];
}
}
return $ordered;
}
/**
* Get category breadcrumb
*
* @param int $idCategory Category ID
* @return string
*/
protected function getCategoryBreadcrumb($idCategory)
{
$sql = new DbQuery();
$sql->select('cl.name');
$sql->from('category', 'c');
$sql->leftJoin('category_lang', 'cl', 'cl.id_category = c.id_category AND cl.id_lang = ' . (int) $this->idLang . ' AND cl.id_shop = ' . (int) $this->idShop);
$sql->where('c.nleft <= (SELECT nleft FROM ' . _DB_PREFIX_ . 'category WHERE id_category = ' . (int) $idCategory . ')');
$sql->where('c.nright >= (SELECT nright FROM ' . _DB_PREFIX_ . 'category WHERE id_category = ' . (int) $idCategory . ')');
$sql->where('c.id_parent > 0');
$sql->orderBy('c.level_depth ASC');
$results = Db::getInstance()->executeS($sql);
if (!$results) {
return '';
}
return implode(' > ', array_column($results, 'name'));
}
/**
* Get full category tree with hierarchy
*
* @param int $idLang Language ID
* @param int $idShop Shop ID
* @param array $selectedIds Currently selected category IDs
* @param bool $activeOnly Only include active categories
* @return array Tree structure
*/
public function getCategoryTree($idLang = null, $idShop = null, array $selectedIds = [], $activeOnly = false)
{
$idLang = $idLang ?: $this->idLang;
$idShop = $idShop ?: $this->idShop;
$sql = new DbQuery();
$sql->select('c.id_category, c.id_parent, cl.name, c.active, c.level_depth, c.position');
$sql->select('(SELECT COUNT(cp.id_product) FROM ' . _DB_PREFIX_ . 'category_product cp WHERE cp.id_category = c.id_category) AS product_count');
$sql->from('category', 'c');
$sql->innerJoin('category_shop', 'cs', 'cs.id_category = c.id_category AND cs.id_shop = ' . (int) $idShop);
$sql->leftJoin('category_lang', 'cl', 'cl.id_category = c.id_category AND cl.id_lang = ' . (int) $idLang . ' AND cl.id_shop = ' . (int) $idShop);
$sql->where('c.id_parent > 0'); // Exclude root
if ($activeOnly) {
$sql->where('c.active = 1');
}
$sql->orderBy('c.level_depth ASC, c.position ASC');
$results = Db::getInstance()->executeS($sql);
if (!$results) {
return [];
}
// Convert selected IDs to lookup set
$selectedSet = array_flip($selectedIds);
// Build flat array with parent references
$categories = [];
foreach ($results as $row) {
$id = (int) $row['id_category'];
$categories[$id] = [
'id' => $id,
'id_parent' => (int) $row['id_parent'],
'name' => $row['name'],
'active' => (bool) $row['active'],
'level_depth' => (int) $row['level_depth'],
'position' => (int) $row['position'],
'product_count' => (int) $row['product_count'],
'selected' => isset($selectedSet[$id]),
'children' => [],
];
}
// Build tree structure
$tree = [];
foreach ($categories as $id => &$category) {
$parentId = $category['id_parent'];
if (isset($categories[$parentId])) {
$categories[$parentId]['children'][] = &$category;
} else {
// Top-level category (parent is root)
$tree[] = &$category;
}
}
unset($category);
// Sort children by position at each level
$this->sortTreeByPosition($tree);
return $tree;
}
/**
* Recursively sort tree children by position
*
* @param array &$nodes Tree nodes
*/
protected function sortTreeByPosition(array &$nodes)
{
usort($nodes, function ($a, $b) {
return $a['position'] - $b['position'];
});
foreach ($nodes as &$node) {
if (!empty($node['children'])) {
$this->sortTreeByPosition($node['children']);
}
}
}
/**
* Get all descendant category IDs for given parent IDs
*
* @param array $parentIds Parent category IDs
* @param int $idShop Shop ID
* @return array All descendant IDs (including parents)
*/
public function getCategoryDescendants(array $parentIds, $idShop = null)
{
if (empty($parentIds)) {
return [];
}
$idShop = $idShop ?: $this->idShop;
$allIds = $parentIds;
// Use nleft/nright for efficient descendant lookup
$sql = new DbQuery();
$sql->select('DISTINCT c2.id_category');
$sql->from('category', 'c1');
$sql->innerJoin('category', 'c2', 'c2.nleft > c1.nleft AND c2.nright < c1.nright');
$sql->innerJoin('category_shop', 'cs', 'cs.id_category = c2.id_category AND cs.id_shop = ' . (int) $idShop);
$sql->where('c1.id_category IN (' . implode(',', array_map('intval', $parentIds)) . ')');
$results = Db::getInstance()->executeS($sql);
if ($results) {
foreach ($results as $row) {
$allIds[] = (int) $row['id_category'];
}
}
return array_unique($allIds);
}
// =========================================================================
// MANUFACTURERS
// =========================================================================
/**
* Search manufacturers
*
* @param string $query Search query
* @param int $idLang Language ID
* @param int $idShop Shop ID
* @param int $limit Results limit
* @param int $offset Results offset
* @param array $filters Additional filters
* @return array Manufacturer results
*/
public function searchTargetManufacturers($query, $idLang, $idShop, $limit = 20, $offset = 0, array $filters = [])
{
$sql = new DbQuery();
$sql->select('DISTINCT m.id_manufacturer, m.name, m.active');
$sql->select('(SELECT COUNT(DISTINCT p.id_product) FROM ' . _DB_PREFIX_ . 'product p
INNER JOIN ' . _DB_PREFIX_ . 'product_shop ps ON ps.id_product = p.id_product AND ps.id_shop = ' . (int) $idShop . '
WHERE p.id_manufacturer = m.id_manufacturer) AS product_count');
$sql->from('manufacturer', 'm');
$sql->innerJoin('manufacturer_shop', 'ms', 'ms.id_manufacturer = m.id_manufacturer AND ms.id_shop = ' . (int) $idShop);
if (!empty($query)) {
$escapedQuery = $this->escapePattern($query);
$sql->where('(m.name LIKE \'%' . $escapedQuery . '%\' OR m.id_manufacturer = ' . (int) $query . ')');
}
if (!empty($filters['refine'])) {
$refine = $this->escapePattern($filters['refine']);
if (!empty($filters['refine_negate'])) {
$sql->where('m.name NOT LIKE \'%' . $refine . '%\'');
} else {
$sql->where('m.name LIKE \'%' . $refine . '%\'');
}
}
// Has products
if (!empty($filters['has_products'])) {
$sql->having('product_count > 0');
}
// Product count range
if ($filters['product_count_min'] !== null || $filters['product_count_max'] !== null) {
$sql->groupBy('m.id_manufacturer');
if ($filters['product_count_min'] !== null) {
$sql->having('product_count >= ' . (int) $filters['product_count_min']);
}
if ($filters['product_count_max'] !== null) {
$sql->having('product_count <= ' . (int) $filters['product_count_max']);
}
}
if (!empty($filters['active_only'])) {
$sql->where('m.active = 1');
}
// Sorting
$manufacturerSortMap = [
'name' => 'm.name',
'id' => 'm.id_manufacturer',
'product_count' => 'product_count',
];
$sql->orderBy($this->buildOrderBy('manufacturers', $filters, $manufacturerSortMap));
$sql->limit((int) $limit, (int) $offset);
$results = Db::getInstance()->executeS($sql);
if (!$results) {
return [];
}
return array_map(function ($row) {
return [
'id' => (int) $row['id_manufacturer'],
'type' => 'manufacturer',
'name' => $row['name'],
'active' => (bool) $row['active'],
'product_count' => (int) $row['product_count'],
];
}, $results);
}
/**
* Count manufacturers
*/
public function countTargetManufacturers($query, $idLang, $idShop, array $filters = [])
{
$sql = new DbQuery();
$sql->select('COUNT(DISTINCT m.id_manufacturer)');
$sql->from('manufacturer', 'm');
$sql->innerJoin('manufacturer_shop', 'ms', 'ms.id_manufacturer = m.id_manufacturer AND ms.id_shop = ' . (int) $idShop);
if (!empty($query)) {
$escapedQuery = $this->escapePattern($query);
$sql->where('(m.name LIKE \'%' . $escapedQuery . '%\' OR m.id_manufacturer = ' . (int) $query . ')');
}
if (!empty($filters['refine'])) {
$refine = $this->escapePattern($filters['refine']);
if (!empty($filters['refine_negate'])) {
$sql->where('m.name NOT LIKE \'%' . $refine . '%\'');
} else {
$sql->where('m.name LIKE \'%' . $refine . '%\'');
}
}
if (!empty($filters['active_only'])) {
$sql->where('m.active = 1');
}
return (int) Db::getInstance()->getValue($sql);
}
/**
* Get manufacturers by IDs
*/
public function getTargetManufacturersByIds(array $ids, $idLang)
{
if (empty($ids)) {
return [];
}
$sql = new DbQuery();
$sql->select('m.id_manufacturer, m.name, m.active');
$sql->from('manufacturer', 'm');
$sql->where('m.id_manufacturer IN (' . implode(',', array_map('intval', $ids)) . ')');
$results = Db::getInstance()->executeS($sql);
if (!$results) {
return [];
}
$manufacturers = [];
foreach ($results as $row) {
$manufacturers[(int) $row['id_manufacturer']] = [
'id' => (int) $row['id_manufacturer'],
'type' => 'manufacturer',
'name' => $row['name'],
'active' => (bool) $row['active'],
];
}
$ordered = [];
foreach ($ids as $id) {
if (isset($manufacturers[(int) $id])) {
$ordered[] = $manufacturers[(int) $id];
}
}
return $ordered;
}
// =========================================================================
// SUPPLIERS
// =========================================================================
/**
* Search suppliers
*/
public function searchTargetSuppliers($query, $idLang, $idShop, $limit = 20, $offset = 0, array $filters = [])
{
$sql = new DbQuery();
$sql->select('DISTINCT s.id_supplier, s.name, s.active');
$sql->select('(SELECT COUNT(DISTINCT ps.id_product) FROM ' . _DB_PREFIX_ . 'product_supplier ps
INNER JOIN ' . _DB_PREFIX_ . 'product_shop psh ON psh.id_product = ps.id_product AND psh.id_shop = ' . (int) $idShop . '
WHERE ps.id_supplier = s.id_supplier) AS product_count');
$sql->from('supplier', 's');
$sql->innerJoin('supplier_shop', 'ss', 'ss.id_supplier = s.id_supplier AND ss.id_shop = ' . (int) $idShop);
if (!empty($query)) {
$escapedQuery = $this->escapePattern($query);
$sql->where('(s.name LIKE \'%' . $escapedQuery . '%\' OR s.id_supplier = ' . (int) $query . ')');
}
if (!empty($filters['refine'])) {
$refine = $this->escapePattern($filters['refine']);
if (!empty($filters['refine_negate'])) {
$sql->where('s.name NOT LIKE \'%' . $refine . '%\'');
} else {
$sql->where('s.name LIKE \'%' . $refine . '%\'');
}
}
if (!empty($filters['active_only'])) {
$sql->where('s.active = 1');
}
// Sorting
$supplierSortMap = [
'name' => 's.name',
'id' => 's.id_supplier',
'product_count' => 'product_count',
];
$sql->orderBy($this->buildOrderBy('suppliers', $filters, $supplierSortMap));
$sql->limit((int) $limit, (int) $offset);
$results = Db::getInstance()->executeS($sql);
if (!$results) {
return [];
}
return array_map(function ($row) {
return [
'id' => (int) $row['id_supplier'],
'type' => 'supplier',
'name' => $row['name'],
'active' => (bool) $row['active'],
'product_count' => (int) $row['product_count'],
];
}, $results);
}
/**
* Count suppliers
*/
public function countTargetSuppliers($query, $idLang, $idShop, array $filters = [])
{
$sql = new DbQuery();
$sql->select('COUNT(DISTINCT s.id_supplier)');
$sql->from('supplier', 's');
$sql->innerJoin('supplier_shop', 'ss', 'ss.id_supplier = s.id_supplier AND ss.id_shop = ' . (int) $idShop);
if (!empty($query)) {
$escapedQuery = $this->escapePattern($query);
$sql->where('(s.name LIKE \'%' . $escapedQuery . '%\' OR s.id_supplier = ' . (int) $query . ')');
}
if (!empty($filters['refine'])) {
$refine = $this->escapePattern($filters['refine']);
if (!empty($filters['refine_negate'])) {
$sql->where('s.name NOT LIKE \'%' . $refine . '%\'');
} else {
$sql->where('s.name LIKE \'%' . $refine . '%\'');
}
}
if (!empty($filters['active_only'])) {
$sql->where('s.active = 1');
}
return (int) Db::getInstance()->getValue($sql);
}
/**
* Get suppliers by IDs
*/
public function getTargetSuppliersByIds(array $ids, $idLang)
{
if (empty($ids)) {
return [];
}
$sql = new DbQuery();
$sql->select('s.id_supplier, s.name, s.active');
$sql->from('supplier', 's');
$sql->where('s.id_supplier IN (' . implode(',', array_map('intval', $ids)) . ')');
$results = Db::getInstance()->executeS($sql);
if (!$results) {
return [];
}
$suppliers = [];
foreach ($results as $row) {
$suppliers[(int) $row['id_supplier']] = [
'id' => (int) $row['id_supplier'],
'type' => 'supplier',
'name' => $row['name'],
'active' => (bool) $row['active'],
];
}
$ordered = [];
foreach ($ids as $id) {
if (isset($suppliers[(int) $id])) {
$ordered[] = $suppliers[(int) $id];
}
}
return $ordered;
}
// =========================================================================
// CMS PAGES
// =========================================================================
/**
* Search CMS pages
*/
public function searchTargetCms($query, $idLang, $idShop, $limit = 20, $offset = 0, array $filters = [])
{
$sql = new DbQuery();
$sql->select('DISTINCT c.id_cms, cl.meta_title, c.active, c.position, c.id_cms_category');
$sql->select('ccl.name AS category_name');
$sql->from('cms', 'c');
$sql->innerJoin('cms_shop', 'cs', 'cs.id_cms = c.id_cms AND cs.id_shop = ' . (int) $idShop);
$sql->leftJoin('cms_lang', 'cl', 'cl.id_cms = c.id_cms AND cl.id_lang = ' . (int) $idLang . ' AND cl.id_shop = ' . (int) $idShop);
$sql->leftJoin('cms_category_lang', 'ccl', 'ccl.id_cms_category = c.id_cms_category AND ccl.id_lang = ' . (int) $idLang . ' AND ccl.id_shop = ' . (int) $idShop);
if (!empty($query)) {
$escapedQuery = $this->escapePattern($query);
$sql->where('(cl.meta_title LIKE \'%' . $escapedQuery . '%\' OR c.id_cms = ' . (int) $query . ')');
}
if (!empty($filters['refine'])) {
$refine = $this->escapePattern($filters['refine']);
if (!empty($filters['refine_negate'])) {
$sql->where('cl.meta_title NOT LIKE \'%' . $refine . '%\'');
} else {
$sql->where('cl.meta_title LIKE \'%' . $refine . '%\'');
}
}
if (!empty($filters['active_only'])) {
$sql->where('c.active = 1');
}
// Sorting
$cmsSortMap = [
'name' => 'cl.meta_title',
'id' => 'c.id_cms',
'position' => 'c.position',
];
$sql->orderBy($this->buildOrderBy('cms', $filters, $cmsSortMap));
$sql->limit((int) $limit, (int) $offset);
$results = Db::getInstance()->executeS($sql);
if (!$results) {
return [];
}
return array_map(function ($row) {
return [
'id' => (int) $row['id_cms'],
'type' => 'cms',
'name' => $row['meta_title'],
'active' => (bool) $row['active'],
'position' => (int) $row['position'],
'category' => $row['category_name'],
];
}, $results);
}
/**
* Count CMS pages
*/
public function countTargetCms($query, $idLang, $idShop, array $filters = [])
{
$sql = new DbQuery();
$sql->select('COUNT(DISTINCT c.id_cms)');
$sql->from('cms', 'c');
$sql->innerJoin('cms_shop', 'cs', 'cs.id_cms = c.id_cms AND cs.id_shop = ' . (int) $idShop);
$sql->leftJoin('cms_lang', 'cl', 'cl.id_cms = c.id_cms AND cl.id_lang = ' . (int) $idLang . ' AND cl.id_shop = ' . (int) $idShop);
if (!empty($query)) {
$escapedQuery = $this->escapePattern($query);
$sql->where('(cl.meta_title LIKE \'%' . $escapedQuery . '%\' OR c.id_cms = ' . (int) $query . ')');
}
if (!empty($filters['refine'])) {
$refine = $this->escapePattern($filters['refine']);
if (!empty($filters['refine_negate'])) {
$sql->where('cl.meta_title NOT LIKE \'%' . $refine . '%\'');
} else {
$sql->where('cl.meta_title LIKE \'%' . $refine . '%\'');
}
}
if (!empty($filters['active_only'])) {
$sql->where('c.active = 1');
}
return (int) Db::getInstance()->getValue($sql);
}
/**
* Get CMS pages by IDs
*/
public function getTargetCmsByIds(array $ids, $idLang)
{
if (empty($ids)) {
return [];
}
$sql = new DbQuery();
$sql->select('c.id_cms, cl.meta_title, c.active');
$sql->from('cms', 'c');
$sql->leftJoin('cms_lang', 'cl', 'cl.id_cms = c.id_cms AND cl.id_lang = ' . (int) $idLang . ' AND cl.id_shop = ' . (int) $this->idShop);
$sql->where('c.id_cms IN (' . implode(',', array_map('intval', $ids)) . ')');
$results = Db::getInstance()->executeS($sql);
if (!$results) {
return [];
}
$cms = [];
foreach ($results as $row) {
$cms[(int) $row['id_cms']] = [
'id' => (int) $row['id_cms'],
'type' => 'cms',
'name' => $row['meta_title'],
'active' => (bool) $row['active'],
];
}
$ordered = [];
foreach ($ids as $id) {
if (isset($cms[(int) $id])) {
$ordered[] = $cms[(int) $id];
}
}
return $ordered;
}
// =========================================================================
// CMS CATEGORIES
// =========================================================================
/**
* Search CMS categories
*/
public function searchTargetCmsCategories($query, $idLang, $idShop, $limit = 20, $offset = 0, array $filters = [])
{
$sql = new DbQuery();
$sql->select('DISTINCT cc.id_cms_category, ccl.name, cc.active, cc.level_depth, cc.position');
$sql->from('cms_category', 'cc');
$sql->innerJoin('cms_category_shop', 'ccs', 'ccs.id_cms_category = cc.id_cms_category AND ccs.id_shop = ' . (int) $idShop);
$sql->leftJoin('cms_category_lang', 'ccl', 'ccl.id_cms_category = cc.id_cms_category AND ccl.id_lang = ' . (int) $idLang . ' AND ccl.id_shop = ' . (int) $idShop);
// Exclude root
$sql->where('cc.id_parent > 0');
if (!empty($query)) {
$escapedQuery = $this->escapePattern($query);
$sql->where('(ccl.name LIKE \'%' . $escapedQuery . '%\' OR cc.id_cms_category = ' . (int) $query . ')');
}
if (!empty($filters['refine'])) {
$refine = $this->escapePattern($filters['refine']);
if (!empty($filters['refine_negate'])) {
$sql->where('ccl.name NOT LIKE \'%' . $refine . '%\'');
} else {
$sql->where('ccl.name LIKE \'%' . $refine . '%\'');
}
}
if (!empty($filters['active_only'])) {
$sql->where('cc.active = 1');
}
// Sorting
$cmsCategorySortMap = [
'name' => 'ccl.name',
'id' => 'cc.id_cms_category',
'position' => 'cc.position',
];
$sql->orderBy($this->buildOrderBy('cms_categories', $filters, $cmsCategorySortMap));
$sql->limit((int) $limit, (int) $offset);
$results = Db::getInstance()->executeS($sql);
if (!$results) {
return [];
}
return array_map(function ($row) {
return [
'id' => (int) $row['id_cms_category'],
'type' => 'cms_category',
'name' => $row['name'],
'active' => (bool) $row['active'],
'level_depth' => (int) $row['level_depth'],
'position' => (int) $row['position'],
];
}, $results);
}
/**
* Count CMS categories
*/
public function countTargetCmsCategories($query, $idLang, $idShop, array $filters = [])
{
$sql = new DbQuery();
$sql->select('COUNT(DISTINCT cc.id_cms_category)');
$sql->from('cms_category', 'cc');
$sql->innerJoin('cms_category_shop', 'ccs', 'ccs.id_cms_category = cc.id_cms_category AND ccs.id_shop = ' . (int) $idShop);
$sql->leftJoin('cms_category_lang', 'ccl', 'ccl.id_cms_category = cc.id_cms_category AND ccl.id_lang = ' . (int) $idLang . ' AND ccl.id_shop = ' . (int) $idShop);
$sql->where('cc.id_parent > 0');
if (!empty($query)) {
$escapedQuery = $this->escapePattern($query);
$sql->where('(ccl.name LIKE \'%' . $escapedQuery . '%\' OR cc.id_cms_category = ' . (int) $query . ')');
}
if (!empty($filters['refine'])) {
$refine = $this->escapePattern($filters['refine']);
if (!empty($filters['refine_negate'])) {
$sql->where('ccl.name NOT LIKE \'%' . $refine . '%\'');
} else {
$sql->where('ccl.name LIKE \'%' . $refine . '%\'');
}
}
if (!empty($filters['active_only'])) {
$sql->where('cc.active = 1');
}
return (int) Db::getInstance()->getValue($sql);
}
/**
* Get CMS categories by IDs
*/
public function getTargetCmsCategoriesByIds(array $ids, $idLang)
{
if (empty($ids)) {
return [];
}
$sql = new DbQuery();
$sql->select('cc.id_cms_category, ccl.name, cc.active');
$sql->from('cms_category', 'cc');
$sql->leftJoin('cms_category_lang', 'ccl', 'ccl.id_cms_category = cc.id_cms_category AND ccl.id_lang = ' . (int) $idLang . ' AND ccl.id_shop = ' . (int) $this->idShop);
$sql->where('cc.id_cms_category IN (' . implode(',', array_map('intval', $ids)) . ')');
$results = Db::getInstance()->executeS($sql);
if (!$results) {
return [];
}
$categories = [];
foreach ($results as $row) {
$categories[(int) $row['id_cms_category']] = [
'id' => (int) $row['id_cms_category'],
'type' => 'cms_category',
'name' => $row['name'],
'active' => (bool) $row['active'],
];
}
$ordered = [];
foreach ($ids as $id) {
if (isset($categories[(int) $id])) {
$ordered[] = $categories[(int) $id];
}
}
return $ordered;
}
// =========================================================================
// EMPLOYEES
// =========================================================================
/**
* Search employees
*/
public function searchTargetEmployees($query, $idLang, $idShop, array $filters = [])
{
$limit = isset($filters['limit']) ? (int) $filters['limit'] : 20;
$sql = new DbQuery();
$sql->select('DISTINCT e.id_employee, e.firstname, e.lastname, e.email, e.active, e.id_profile');
$sql->select('pl.name AS profile_name');
$sql->from('employee', 'e');
$sql->leftJoin('profile_lang', 'pl', 'pl.id_profile = e.id_profile AND pl.id_lang = ' . (int) $idLang);
if (!empty($query)) {
$escapedQuery = $this->escapePattern($query);
$sql->where('(e.firstname LIKE \'%' . $escapedQuery . '%\' OR e.lastname LIKE \'%' . $escapedQuery . '%\' OR e.email LIKE \'%' . $escapedQuery . '%\' OR e.id_employee = ' . (int) $query . ')');
}
if (!empty($filters['refine'])) {
$refine = $this->escapePattern($filters['refine']);
if (!empty($filters['refine_negate'])) {
$sql->where('CONCAT(e.firstname, \' \', e.lastname) NOT LIKE \'%' . $refine . '%\'');
} else {
$sql->where('CONCAT(e.firstname, \' \', e.lastname) LIKE \'%' . $refine . '%\'');
}
}
if (!empty($filters['active_only'])) {
$sql->where('e.active = 1');
}
// Sorting
$employeeSortMap = [
'name' => 'e.lastname',
'id' => 'e.id_employee',
];
$sql->orderBy($this->buildOrderBy('employees', $filters, $employeeSortMap));
$sql->limit($limit);
$results = Db::getInstance()->executeS($sql);
if (!$results) {
return [];
}
return array_map(function ($row) {
return [
'id' => (int) $row['id_employee'],
'type' => 'employee',
'name' => $row['firstname'] . ' ' . $row['lastname'],
'email' => $row['email'],
'active' => (bool) $row['active'],
'profile' => $row['profile_name'],
];
}, $results);
}
/**
* Count employees
*/
public function countTargetEmployees($query, $idLang, $idShop, array $filters = [])
{
$sql = new DbQuery();
$sql->select('COUNT(DISTINCT e.id_employee)');
$sql->from('employee', 'e');
if (!empty($query)) {
$escapedQuery = $this->escapePattern($query);
$sql->where('(e.firstname LIKE \'%' . $escapedQuery . '%\' OR e.lastname LIKE \'%' . $escapedQuery . '%\' OR e.email LIKE \'%' . $escapedQuery . '%\' OR e.id_employee = ' . (int) $query . ')');
}
if (!empty($filters['refine'])) {
$refine = $this->escapePattern($filters['refine']);
if (!empty($filters['refine_negate'])) {
$sql->where('CONCAT(e.firstname, \' \', e.lastname) NOT LIKE \'%' . $refine . '%\'');
} else {
$sql->where('CONCAT(e.firstname, \' \', e.lastname) LIKE \'%' . $refine . '%\'');
}
}
if (!empty($filters['active_only'])) {
$sql->where('e.active = 1');
}
return (int) Db::getInstance()->getValue($sql);
}
/**
* Get employees by IDs
*/
public function getTargetEmployeesByIds(array $ids, $idLang)
{
if (empty($ids)) {
return [];
}
$sql = new DbQuery();
$sql->select('e.id_employee, e.firstname, e.lastname, e.email, e.active');
$sql->from('employee', 'e');
$sql->where('e.id_employee IN (' . implode(',', array_map('intval', $ids)) . ')');
$results = Db::getInstance()->executeS($sql);
if (!$results) {
return [];
}
$employees = [];
foreach ($results as $row) {
$employees[(int) $row['id_employee']] = [
'id' => (int) $row['id_employee'],
'type' => 'employee',
'name' => $row['firstname'] . ' ' . $row['lastname'],
'email' => $row['email'],
'active' => (bool) $row['active'],
];
}
$ordered = [];
foreach ($ids as $id) {
if (isset($employees[(int) $id])) {
$ordered[] = $employees[(int) $id];
}
}
return $ordered;
}
// =========================================================================
// CUSTOMERS
// =========================================================================
/**
* Search customers
*/
public function searchTargetCustomers($query, $idLang, $idShop, $limit = 20, $offset = 0, array $filters = [])
{
$sql = new DbQuery();
$sql->select('DISTINCT c.id_customer, c.firstname, c.lastname, c.email, c.active, c.newsletter, c.company');
$sql->select('c.date_add, gl.name AS group_name');
$sql->from('customer', 'c');
$sql->leftJoin('group_lang', 'gl', 'gl.id_group = c.id_default_group AND gl.id_lang = ' . (int) $idLang);
$sql->where('c.id_shop = ' . (int) $idShop);
$sql->where('c.deleted = 0');
if (!empty($query)) {
$escapedQuery = $this->escapePattern($query);
$sql->where('(c.firstname LIKE \'%' . $escapedQuery . '%\' OR c.lastname LIKE \'%' . $escapedQuery . '%\' OR c.email LIKE \'%' . $escapedQuery . '%\' OR c.company LIKE \'%' . $escapedQuery . '%\' OR c.id_customer = ' . (int) $query . ')');
}
if (!empty($filters['refine'])) {
$refine = $this->escapePattern($filters['refine']);
if (!empty($filters['refine_negate'])) {
$sql->where('CONCAT(c.firstname, \' \', c.lastname) NOT LIKE \'%' . $refine . '%\'');
} else {
$sql->where('CONCAT(c.firstname, \' \', c.lastname) LIKE \'%' . $refine . '%\'');
}
}
if (!empty($filters['active_only'])) {
$sql->where('c.active = 1');
}
// Sorting
$customerSortMap = [
'name' => 'c.lastname',
'id' => 'c.id_customer',
];
$sql->orderBy($this->buildOrderBy('customers', $filters, $customerSortMap));
$sql->limit((int) $limit, (int) $offset);
$results = Db::getInstance()->executeS($sql);
if (!$results) {
return [];
}
return array_map(function ($row) {
return [
'id' => (int) $row['id_customer'],
'type' => 'customer',
'name' => $row['firstname'] . ' ' . $row['lastname'],
'email' => $row['email'],
'company' => $row['company'],
'active' => (bool) $row['active'],
'newsletter' => (bool) $row['newsletter'],
'group' => $row['group_name'],
'date_add' => $row['date_add'],
];
}, $results);
}
/**
* Count customers
*/
public function countTargetCustomers($query, $idLang, $idShop, array $filters = [])
{
$sql = new DbQuery();
$sql->select('COUNT(DISTINCT c.id_customer)');
$sql->from('customer', 'c');
$sql->where('c.id_shop = ' . (int) $idShop);
$sql->where('c.deleted = 0');
if (!empty($query)) {
$escapedQuery = $this->escapePattern($query);
$sql->where('(c.firstname LIKE \'%' . $escapedQuery . '%\' OR c.lastname LIKE \'%' . $escapedQuery . '%\' OR c.email LIKE \'%' . $escapedQuery . '%\' OR c.company LIKE \'%' . $escapedQuery . '%\' OR c.id_customer = ' . (int) $query . ')');
}
if (!empty($filters['refine'])) {
$refine = $this->escapePattern($filters['refine']);
if (!empty($filters['refine_negate'])) {
$sql->where('CONCAT(c.firstname, \' \', c.lastname) NOT LIKE \'%' . $refine . '%\'');
} else {
$sql->where('CONCAT(c.firstname, \' \', c.lastname) LIKE \'%' . $refine . '%\'');
}
}
if (!empty($filters['active_only'])) {
$sql->where('c.active = 1');
}
return (int) Db::getInstance()->getValue($sql);
}
/**
* Get customers by IDs
*/
public function getTargetCustomersByIds(array $ids, $idLang)
{
if (empty($ids)) {
return [];
}
$sql = new DbQuery();
$sql->select('c.id_customer, c.firstname, c.lastname, c.email, c.active');
$sql->from('customer', 'c');
$sql->where('c.id_customer IN (' . implode(',', array_map('intval', $ids)) . ')');
$results = Db::getInstance()->executeS($sql);
if (!$results) {
return [];
}
$customers = [];
foreach ($results as $row) {
$customers[(int) $row['id_customer']] = [
'id' => (int) $row['id_customer'],
'type' => 'customer',
'name' => $row['firstname'] . ' ' . $row['lastname'],
'email' => $row['email'],
'active' => (bool) $row['active'],
];
}
$ordered = [];
foreach ($ids as $id) {
if (isset($customers[(int) $id])) {
$ordered[] = $customers[(int) $id];
}
}
return $ordered;
}
// =========================================================================
// CUSTOMER GROUPS
// =========================================================================
/**
* Search customer groups
*/
public function searchTargetCustomerGroups($query, $idLang, $idShop, array $filters = [])
{
$limit = isset($filters['limit']) ? (int) $filters['limit'] : 20;
$sql = new DbQuery();
$sql->select('DISTINCT g.id_group, gl.name, g.reduction, g.price_display_method, g.show_prices');
$sql->select('(SELECT COUNT(cg.id_customer) FROM ' . _DB_PREFIX_ . 'customer_group cg WHERE cg.id_group = g.id_group) AS customer_count');
$sql->from('group', 'g');
$sql->innerJoin('group_shop', 'gs', 'gs.id_group = g.id_group AND gs.id_shop = ' . (int) $idShop);
$sql->leftJoin('group_lang', 'gl', 'gl.id_group = g.id_group AND gl.id_lang = ' . (int) $idLang);
if (!empty($query)) {
$escapedQuery = $this->escapePattern($query);
$sql->where('(gl.name LIKE \'%' . $escapedQuery . '%\' OR g.id_group = ' . (int) $query . ')');
}
if (!empty($filters['refine'])) {
$refine = $this->escapePattern($filters['refine']);
if (!empty($filters['refine_negate'])) {
$sql->where('gl.name NOT LIKE \'%' . $refine . '%\'');
} else {
$sql->where('gl.name LIKE \'%' . $refine . '%\'');
}
}
// Sorting
$customerGroupSortMap = [
'name' => 'gl.name',
'id' => 'g.id_group',
];
$sql->orderBy($this->buildOrderBy('customer_groups', $filters, $customerGroupSortMap));
$sql->limit($limit);
$results = Db::getInstance()->executeS($sql);
if (!$results) {
return [];
}
return array_map(function ($row) {
return [
'id' => (int) $row['id_group'],
'type' => 'customer_group',
'name' => $row['name'],
'reduction' => (float) $row['reduction'],
'price_display' => (int) $row['price_display_method'],
'show_prices' => (bool) $row['show_prices'],
'customer_count' => (int) $row['customer_count'],
];
}, $results);
}
/**
* Count customer groups
*/
public function countTargetCustomerGroups($query, $idLang, $idShop, array $filters = [])
{
$sql = new DbQuery();
$sql->select('COUNT(DISTINCT g.id_group)');
$sql->from('group', 'g');
$sql->innerJoin('group_shop', 'gs', 'gs.id_group = g.id_group AND gs.id_shop = ' . (int) $idShop);
$sql->leftJoin('group_lang', 'gl', 'gl.id_group = g.id_group AND gl.id_lang = ' . (int) $idLang);
if (!empty($query)) {
$escapedQuery = $this->escapePattern($query);
$sql->where('(gl.name LIKE \'%' . $escapedQuery . '%\' OR g.id_group = ' . (int) $query . ')');
}
if (!empty($filters['refine'])) {
$refine = $this->escapePattern($filters['refine']);
if (!empty($filters['refine_negate'])) {
$sql->where('gl.name NOT LIKE \'%' . $refine . '%\'');
} else {
$sql->where('gl.name LIKE \'%' . $refine . '%\'');
}
}
return (int) Db::getInstance()->getValue($sql);
}
/**
* Get customer groups by IDs
*/
public function getTargetCustomerGroupsByIds(array $ids, $idLang)
{
if (empty($ids)) {
return [];
}
$sql = new DbQuery();
$sql->select('g.id_group, gl.name');
$sql->from('group', 'g');
$sql->leftJoin('group_lang', 'gl', 'gl.id_group = g.id_group AND gl.id_lang = ' . (int) $idLang);
$sql->where('g.id_group IN (' . implode(',', array_map('intval', $ids)) . ')');
$results = Db::getInstance()->executeS($sql);
if (!$results) {
return [];
}
$groups = [];
foreach ($results as $row) {
$groups[(int) $row['id_group']] = [
'id' => (int) $row['id_group'],
'type' => 'customer_group',
'name' => $row['name'],
];
}
$ordered = [];
foreach ($ids as $id) {
if (isset($groups[(int) $id])) {
$ordered[] = $groups[(int) $id];
}
}
return $ordered;
}
// =========================================================================
// CARRIERS
// =========================================================================
/**
* Search carriers
*/
public function searchTargetCarriers($query, $idLang, $idShop, array $filters = [])
{
$limit = isset($filters['limit']) ? (int) $filters['limit'] : 20;
$sql = new DbQuery();
$sql->select('DISTINCT c.id_carrier, c.id_reference, c.name, c.active, c.is_free, c.shipping_handling');
$sql->from('carrier', 'c');
$sql->where('c.deleted = 0');
if (!empty($query)) {
$escapedQuery = $this->escapePattern($query);
$sql->where('(c.name LIKE \'%' . $escapedQuery . '%\' OR c.id_carrier = ' . (int) $query . ')');
}
if (!empty($filters['refine'])) {
$refine = $this->escapePattern($filters['refine']);
if (!empty($filters['refine_negate'])) {
$sql->where('c.name NOT LIKE \'%' . $refine . '%\'');
} else {
$sql->where('c.name LIKE \'%' . $refine . '%\'');
}
}
if (!empty($filters['active_only'])) {
$sql->where('c.active = 1');
}
// Sorting
$carrierSortMap = [
'name' => 'c.name',
'id' => 'c.id_carrier',
'position' => 'c.position',
];
$sql->orderBy($this->buildOrderBy('carriers', $filters, $carrierSortMap));
$sql->limit($limit);
$results = Db::getInstance()->executeS($sql);
if (!$results) {
return [];
}
return array_map(function ($row) {
return [
'id' => (int) $row['id_carrier'],
'type' => 'carrier',
'id_reference' => (int) $row['id_reference'],
'name' => $row['name'],
'active' => (bool) $row['active'],
'is_free' => (bool) $row['is_free'],
'shipping_handling' => (bool) $row['shipping_handling'],
];
}, $results);
}
/**
* Count carriers
*/
public function countTargetCarriers($query, $idLang, $idShop, array $filters = [])
{
$sql = new DbQuery();
$sql->select('COUNT(DISTINCT c.id_carrier)');
$sql->from('carrier', 'c');
$sql->where('c.deleted = 0');
if (!empty($query)) {
$escapedQuery = $this->escapePattern($query);
$sql->where('(c.name LIKE \'%' . $escapedQuery . '%\' OR c.id_carrier = ' . (int) $query . ')');
}
if (!empty($filters['refine'])) {
$refine = $this->escapePattern($filters['refine']);
if (!empty($filters['refine_negate'])) {
$sql->where('c.name NOT LIKE \'%' . $refine . '%\'');
} else {
$sql->where('c.name LIKE \'%' . $refine . '%\'');
}
}
if (!empty($filters['active_only'])) {
$sql->where('c.active = 1');
}
return (int) Db::getInstance()->getValue($sql);
}
/**
* Get carriers by IDs
*/
public function getTargetCarriersByIds(array $ids, $idLang)
{
if (empty($ids)) {
return [];
}
$sql = new DbQuery();
$sql->select('c.id_carrier, c.name, c.active');
$sql->from('carrier', 'c');
$sql->where('c.id_carrier IN (' . implode(',', array_map('intval', $ids)) . ')');
$results = Db::getInstance()->executeS($sql);
if (!$results) {
return [];
}
$carriers = [];
foreach ($results as $row) {
$carriers[(int) $row['id_carrier']] = [
'id' => (int) $row['id_carrier'],
'type' => 'carrier',
'name' => $row['name'],
'active' => (bool) $row['active'],
];
}
$ordered = [];
foreach ($ids as $id) {
if (isset($carriers[(int) $id])) {
$ordered[] = $carriers[(int) $id];
}
}
return $ordered;
}
// =========================================================================
// ZONES
// =========================================================================
/**
* Search zones
*/
public function searchTargetZones($query, $idLang, $idShop, array $filters = [])
{
$limit = isset($filters['limit']) ? (int) $filters['limit'] : 20;
$sql = new DbQuery();
$sql->select('DISTINCT z.id_zone, z.name, z.active');
$sql->from('zone', 'z');
$sql->innerJoin('zone_shop', 'zs', 'zs.id_zone = z.id_zone AND zs.id_shop = ' . (int) $idShop);
if (!empty($query)) {
$escapedQuery = $this->escapePattern($query);
$sql->where('(z.name LIKE \'%' . $escapedQuery . '%\' OR z.id_zone = ' . (int) $query . ')');
}
if (!empty($filters['refine'])) {
$refine = $this->escapePattern($filters['refine']);
if (!empty($filters['refine_negate'])) {
$sql->where('z.name NOT LIKE \'%' . $refine . '%\'');
} else {
$sql->where('z.name LIKE \'%' . $refine . '%\'');
}
}
if (!empty($filters['active_only'])) {
$sql->where('z.active = 1');
}
// Sorting
$zoneSortMap = [
'name' => 'z.name',
'id' => 'z.id_zone',
];
$sql->orderBy($this->buildOrderBy('zones', $filters, $zoneSortMap));
$sql->limit($limit);
$results = Db::getInstance()->executeS($sql);
if (!$results) {
return [];
}
return array_map(function ($row) {
return [
'id' => (int) $row['id_zone'],
'type' => 'zone',
'name' => $row['name'],
'active' => (bool) $row['active'],
];
}, $results);
}
/**
* Count zones
*/
public function countTargetZones($query, $idLang, $idShop, array $filters = [])
{
$sql = new DbQuery();
$sql->select('COUNT(DISTINCT z.id_zone)');
$sql->from('zone', 'z');
$sql->innerJoin('zone_shop', 'zs', 'zs.id_zone = z.id_zone AND zs.id_shop = ' . (int) $idShop);
if (!empty($query)) {
$escapedQuery = $this->escapePattern($query);
$sql->where('(z.name LIKE \'%' . $escapedQuery . '%\' OR z.id_zone = ' . (int) $query . ')');
}
if (!empty($filters['refine'])) {
$refine = $this->escapePattern($filters['refine']);
if (!empty($filters['refine_negate'])) {
$sql->where('z.name NOT LIKE \'%' . $refine . '%\'');
} else {
$sql->where('z.name LIKE \'%' . $refine . '%\'');
}
}
if (!empty($filters['active_only'])) {
$sql->where('z.active = 1');
}
return (int) Db::getInstance()->getValue($sql);
}
/**
* Get zones by IDs
*/
public function getTargetZonesByIds(array $ids, $idLang)
{
if (empty($ids)) {
return [];
}
$sql = new DbQuery();
$sql->select('z.id_zone, z.name, z.active');
$sql->from('zone', 'z');
$sql->where('z.id_zone IN (' . implode(',', array_map('intval', $ids)) . ')');
$results = Db::getInstance()->executeS($sql);
if (!$results) {
return [];
}
$zones = [];
foreach ($results as $row) {
$zones[(int) $row['id_zone']] = [
'id' => (int) $row['id_zone'],
'type' => 'zone',
'name' => $row['name'],
'active' => (bool) $row['active'],
];
}
$ordered = [];
foreach ($ids as $id) {
if (isset($zones[(int) $id])) {
$ordered[] = $zones[(int) $id];
}
}
return $ordered;
}
// =========================================================================
// COUNTRIES
// =========================================================================
/**
* Search countries
*/
public function searchTargetCountries($query, $idLang, $idShop, $limit = 20, $offset = 0, array $filters = [])
{
$sql = new DbQuery();
$sql->select('DISTINCT c.id_country, cl.name, c.iso_code, c.active, c.id_zone, c.contains_states, c.need_zip_code');
$sql->select('z.name AS zone_name');
$sql->from('country', 'c');
$sql->innerJoin('country_shop', 'cs', 'cs.id_country = c.id_country AND cs.id_shop = ' . (int) $idShop);
$sql->leftJoin('country_lang', 'cl', 'cl.id_country = c.id_country AND cl.id_lang = ' . (int) $idLang);
$sql->leftJoin('zone', 'z', 'z.id_zone = c.id_zone');
if (!empty($query)) {
$escapedQuery = $this->escapePattern($query);
$sql->where('(cl.name LIKE \'%' . $escapedQuery . '%\' OR c.iso_code LIKE \'%' . $escapedQuery . '%\' OR c.id_country = ' . (int) $query . ')');
}
if (!empty($filters['refine'])) {
$refine = $this->escapePattern($filters['refine']);
if (!empty($filters['refine_negate'])) {
$sql->where('cl.name NOT LIKE \'%' . $refine . '%\'');
} else {
$sql->where('cl.name LIKE \'%' . $refine . '%\'');
}
}
if (!empty($filters['active_only'])) {
$sql->where('c.active = 1');
}
// Zone filter
if (!empty($filters['zone'])) {
$sql->where('c.id_zone = ' . (int) $filters['zone']);
}
// Contains states filter
if (!empty($filters['contains_states'])) {
$sql->where('c.contains_states = 1');
}
// Has holidays filter - check if PublicHoliday class exists and country has holidays
if (!empty($filters['has_holidays'])) {
if (class_exists('MyPrestaRocks\\PublicHolidays\\PublicHoliday')) {
$countriesWithHolidays = \MyPrestaRocks\PublicHolidays\PublicHoliday::getCountriesWithHolidays();
if (!empty($countriesWithHolidays)) {
$sql->where('c.id_country IN (' . implode(',', array_map('intval', $countriesWithHolidays)) . ')');
} else {
// No countries with holidays, return empty
$sql->where('1 = 0');
}
}
}
// Sorting
$countrySortMap = [
'name' => 'cl.name',
'id' => 'c.id_country',
'zone' => 'z.name',
'iso_code' => 'c.iso_code',
];
$sql->orderBy($this->buildOrderBy('countries', $filters, $countrySortMap));
$sql->limit($limit, $offset);
$results = Db::getInstance()->executeS($sql);
if (!$results) {
return [];
}
return array_map(function ($row) {
return [
'id' => (int) $row['id_country'],
'type' => 'country',
'name' => $row['name'],
'iso_code' => $row['iso_code'],
'active' => (bool) $row['active'],
'zone' => $row['zone_name'],
'contains_states' => (bool) $row['contains_states'],
'need_zip_code' => (bool) $row['need_zip_code'],
];
}, $results);
}
/**
* Count countries
*/
public function countTargetCountries($query, $idLang, $idShop, array $filters = [])
{
$sql = new DbQuery();
$sql->select('COUNT(DISTINCT c.id_country)');
$sql->from('country', 'c');
$sql->innerJoin('country_shop', 'cs', 'cs.id_country = c.id_country AND cs.id_shop = ' . (int) $idShop);
$sql->leftJoin('country_lang', 'cl', 'cl.id_country = c.id_country AND cl.id_lang = ' . (int) $idLang);
if (!empty($query)) {
$escapedQuery = $this->escapePattern($query);
$sql->where('(cl.name LIKE \'%' . $escapedQuery . '%\' OR c.iso_code LIKE \'%' . $escapedQuery . '%\' OR c.id_country = ' . (int) $query . ')');
}
if (!empty($filters['refine'])) {
$refine = $this->escapePattern($filters['refine']);
if (!empty($filters['refine_negate'])) {
$sql->where('cl.name NOT LIKE \'%' . $refine . '%\'');
} else {
$sql->where('cl.name LIKE \'%' . $refine . '%\'');
}
}
if (!empty($filters['active_only'])) {
$sql->where('c.active = 1');
}
// Zone filter
if (!empty($filters['zone'])) {
$sql->where('c.id_zone = ' . (int) $filters['zone']);
}
// Contains states filter
if (!empty($filters['contains_states'])) {
$sql->where('c.contains_states = 1');
}
// Has holidays filter
if (!empty($filters['has_holidays'])) {
if (class_exists('MyPrestaRocks\\PublicHolidays\\PublicHoliday')) {
$countriesWithHolidays = \MyPrestaRocks\PublicHolidays\PublicHoliday::getCountriesWithHolidays();
if (!empty($countriesWithHolidays)) {
$sql->where('c.id_country IN (' . implode(',', array_map('intval', $countriesWithHolidays)) . ')');
} else {
$sql->where('1 = 0');
}
}
}
return (int) Db::getInstance()->getValue($sql);
}
/**
* Get countries by IDs
*/
public function getTargetCountriesByIds(array $ids, $idLang)
{
if (empty($ids)) {
return [];
}
$sql = new DbQuery();
$sql->select('c.id_country, cl.name, c.iso_code, c.active');
$sql->from('country', 'c');
$sql->leftJoin('country_lang', 'cl', 'cl.id_country = c.id_country AND cl.id_lang = ' . (int) $idLang);
$sql->where('c.id_country IN (' . implode(',', array_map('intval', $ids)) . ')');
$results = Db::getInstance()->executeS($sql);
if (!$results) {
return [];
}
$countries = [];
foreach ($results as $row) {
$countries[(int) $row['id_country']] = [
'id' => (int) $row['id_country'],
'type' => 'country',
'name' => $row['name'],
'iso_code' => $row['iso_code'],
'active' => (bool) $row['active'],
];
}
$ordered = [];
foreach ($ids as $id) {
if (isset($countries[(int) $id])) {
$ordered[] = $countries[(int) $id];
}
}
return $ordered;
}
// =========================================================================
// CURRENCIES
// =========================================================================
/**
* Search currencies
*/
public function searchTargetCurrencies($query, $idLang, $idShop, array $filters = [])
{
$limit = isset($filters['limit']) ? (int) $filters['limit'] : 20;
$sql = new DbQuery();
$sql->select('DISTINCT c.id_currency, c.iso_code, c.active, c.conversion_rate, cl.name, cl.symbol');
$sql->from('currency', 'c');
$sql->innerJoin('currency_shop', 'cs', 'cs.id_currency = c.id_currency AND cs.id_shop = ' . (int) $idShop);
$sql->leftJoin('currency_lang', 'cl', 'cl.id_currency = c.id_currency AND cl.id_lang = ' . (int) $idLang);
if (!empty($query)) {
$escapedQuery = $this->escapePattern($query);
$sql->where('(cl.name LIKE \'%' . $escapedQuery . '%\' OR c.iso_code LIKE \'%' . $escapedQuery . '%\' OR c.id_currency = ' . (int) $query . ')');
}
if (!empty($filters['refine'])) {
$refine = $this->escapePattern($filters['refine']);
if (!empty($filters['refine_negate'])) {
$sql->where('cl.name NOT LIKE \'%' . $refine . '%\'');
} else {
$sql->where('cl.name LIKE \'%' . $refine . '%\'');
}
}
if (!empty($filters['active_only'])) {
$sql->where('c.active = 1');
}
// Sorting
$currencySortMap = [
'name' => 'cl.name',
'id' => 'c.id_currency',
'iso_code' => 'c.iso_code',
'conversion_rate' => 'c.conversion_rate',
];
$sql->orderBy($this->buildOrderBy('currencies', $filters, $currencySortMap));
$sql->limit($limit);
$results = Db::getInstance()->executeS($sql);
if (!$results) {
return [];
}
return array_map(function ($row) {
return [
'id' => (int) $row['id_currency'],
'type' => 'currency',
'name' => $row['name'] ?: $row['iso_code'],
'iso_code' => $row['iso_code'],
'symbol' => $row['symbol'] ?: $row['iso_code'],
'active' => (bool) $row['active'],
'conversion_rate' => (float) $row['conversion_rate'],
];
}, $results);
}
/**
* Count currencies
*/
public function countTargetCurrencies($query, $idLang, $idShop, array $filters = [])
{
$sql = new DbQuery();
$sql->select('COUNT(DISTINCT c.id_currency)');
$sql->from('currency', 'c');
$sql->innerJoin('currency_shop', 'cs', 'cs.id_currency = c.id_currency AND cs.id_shop = ' . (int) $idShop);
$sql->leftJoin('currency_lang', 'cl', 'cl.id_currency = c.id_currency AND cl.id_lang = ' . (int) $idLang);
if (!empty($query)) {
$escapedQuery = $this->escapePattern($query);
$sql->where('(cl.name LIKE \'%' . $escapedQuery . '%\' OR c.iso_code LIKE \'%' . $escapedQuery . '%\' OR c.id_currency = ' . (int) $query . ')');
}
if (!empty($filters['refine'])) {
$refine = $this->escapePattern($filters['refine']);
if (!empty($filters['refine_negate'])) {
$sql->where('cl.name NOT LIKE \'%' . $refine . '%\'');
} else {
$sql->where('cl.name LIKE \'%' . $refine . '%\'');
}
}
if (!empty($filters['active_only'])) {
$sql->where('c.active = 1');
}
return (int) Db::getInstance()->getValue($sql);
}
/**
* Get currencies by IDs
*/
public function getTargetCurrenciesByIds(array $ids, $idLang)
{
if (empty($ids)) {
return [];
}
$sql = new DbQuery();
$sql->select('c.id_currency, c.iso_code, c.active, cl.name, cl.symbol');
$sql->from('currency', 'c');
$sql->leftJoin('currency_lang', 'cl', 'cl.id_currency = c.id_currency AND cl.id_lang = ' . (int) $idLang);
$sql->where('c.id_currency IN (' . implode(',', array_map('intval', $ids)) . ')');
$results = Db::getInstance()->executeS($sql);
if (!$results) {
return [];
}
$currencies = [];
foreach ($results as $row) {
$currencies[(int) $row['id_currency']] = [
'id' => (int) $row['id_currency'],
'type' => 'currency',
'name' => $row['name'] ?: $row['iso_code'],
'iso_code' => $row['iso_code'],
'symbol' => $row['symbol'] ?: $row['iso_code'],
'active' => (bool) $row['active'],
];
}
$ordered = [];
foreach ($ids as $id) {
if (isset($currencies[(int) $id])) {
$ordered[] = $currencies[(int) $id];
}
}
return $ordered;
}
// =========================================================================
// LANGUAGES
// =========================================================================
/**
* Search languages
*/
public function searchTargetLanguages($query, $idLang, $idShop, array $filters = [])
{
$limit = isset($filters['limit']) ? (int) $filters['limit'] : 20;
$sql = new DbQuery();
$sql->select('DISTINCT l.id_lang, l.name, l.iso_code, l.active, l.is_rtl, l.locale');
$sql->from('lang', 'l');
$sql->innerJoin('lang_shop', 'ls', 'ls.id_lang = l.id_lang AND ls.id_shop = ' . (int) $idShop);
if (!empty($query)) {
$escapedQuery = $this->escapePattern($query);
$sql->where('(l.name LIKE \'%' . $escapedQuery . '%\' OR l.iso_code LIKE \'%' . $escapedQuery . '%\' OR l.id_lang = ' . (int) $query . ')');
}
if (!empty($filters['refine'])) {
$refine = $this->escapePattern($filters['refine']);
if (!empty($filters['refine_negate'])) {
$sql->where('l.name NOT LIKE \'%' . $refine . '%\'');
} else {
$sql->where('l.name LIKE \'%' . $refine . '%\'');
}
}
if (!empty($filters['active_only'])) {
$sql->where('l.active = 1');
}
// Sorting
$languageSortMap = [
'name' => 'l.name',
'id' => 'l.id_lang',
'iso_code' => 'l.iso_code',
];
$sql->orderBy($this->buildOrderBy('languages', $filters, $languageSortMap));
$sql->limit($limit);
$results = Db::getInstance()->executeS($sql);
if (!$results) {
return [];
}
return array_map(function ($row) {
return [
'id' => (int) $row['id_lang'],
'type' => 'language',
'name' => $row['name'],
'iso_code' => $row['iso_code'],
'locale' => $row['locale'],
'active' => (bool) $row['active'],
'is_rtl' => (bool) $row['is_rtl'],
];
}, $results);
}
/**
* Count languages
*/
public function countTargetLanguages($query, $idLang, $idShop, array $filters = [])
{
$sql = new DbQuery();
$sql->select('COUNT(DISTINCT l.id_lang)');
$sql->from('lang', 'l');
$sql->innerJoin('lang_shop', 'ls', 'ls.id_lang = l.id_lang AND ls.id_shop = ' . (int) $idShop);
if (!empty($query)) {
$escapedQuery = $this->escapePattern($query);
$sql->where('(l.name LIKE \'%' . $escapedQuery . '%\' OR l.iso_code LIKE \'%' . $escapedQuery . '%\' OR l.id_lang = ' . (int) $query . ')');
}
if (!empty($filters['refine'])) {
$refine = $this->escapePattern($filters['refine']);
if (!empty($filters['refine_negate'])) {
$sql->where('l.name NOT LIKE \'%' . $refine . '%\'');
} else {
$sql->where('l.name LIKE \'%' . $refine . '%\'');
}
}
if (!empty($filters['active_only'])) {
$sql->where('l.active = 1');
}
return (int) Db::getInstance()->getValue($sql);
}
/**
* Get languages by IDs
*/
public function getTargetLanguagesByIds(array $ids, $idLang)
{
if (empty($ids)) {
return [];
}
$sql = new DbQuery();
$sql->select('l.id_lang, l.name, l.iso_code, l.active');
$sql->from('lang', 'l');
$sql->where('l.id_lang IN (' . implode(',', array_map('intval', $ids)) . ')');
$results = Db::getInstance()->executeS($sql);
if (!$results) {
return [];
}
$languages = [];
foreach ($results as $row) {
$languages[(int) $row['id_lang']] = [
'id' => (int) $row['id_lang'],
'type' => 'language',
'name' => $row['name'],
'iso_code' => $row['iso_code'],
'active' => (bool) $row['active'],
];
}
$ordered = [];
foreach ($ids as $id) {
if (isset($languages[(int) $id])) {
$ordered[] = $languages[(int) $id];
}
}
return $ordered;
}
// =========================================================================
// SHOPS
// =========================================================================
/**
* Search shops
*/
public function searchTargetShops($query, $idLang, $idShop, array $filters = [])
{
$limit = isset($filters['limit']) ? (int) $filters['limit'] : 20;
$sql = new DbQuery();
$sql->select('DISTINCT s.id_shop, s.name, s.active, s.id_shop_group');
$sql->select('sg.name AS group_name');
$sql->from('shop', 's');
$sql->leftJoin('shop_group', 'sg', 'sg.id_shop_group = s.id_shop_group');
if (!empty($query)) {
$escapedQuery = $this->escapePattern($query);
$sql->where('(s.name LIKE \'%' . $escapedQuery . '%\' OR s.id_shop = ' . (int) $query . ')');
}
if (!empty($filters['refine'])) {
$refine = $this->escapePattern($filters['refine']);
if (!empty($filters['refine_negate'])) {
$sql->where('s.name NOT LIKE \'%' . $refine . '%\'');
} else {
$sql->where('s.name LIKE \'%' . $refine . '%\'');
}
}
if (!empty($filters['active_only'])) {
$sql->where('s.active = 1');
}
// Sorting
$shopSortMap = [
'name' => 's.name',
'id' => 's.id_shop',
'group' => 'sg.name',
];
$sql->orderBy($this->buildOrderBy('shops', $filters, $shopSortMap));
$sql->limit($limit);
$results = Db::getInstance()->executeS($sql);
if (!$results) {
return [];
}
return array_map(function ($row) {
return [
'id' => (int) $row['id_shop'],
'type' => 'shop',
'name' => $row['name'],
'active' => (bool) $row['active'],
'group' => $row['group_name'],
];
}, $results);
}
/**
* Count shops
*/
public function countTargetShops($query, $idLang, $idShop, array $filters = [])
{
$sql = new DbQuery();
$sql->select('COUNT(DISTINCT s.id_shop)');
$sql->from('shop', 's');
if (!empty($query)) {
$escapedQuery = $this->escapePattern($query);
$sql->where('(s.name LIKE \'%' . $escapedQuery . '%\' OR s.id_shop = ' . (int) $query . ')');
}
if (!empty($filters['refine'])) {
$refine = $this->escapePattern($filters['refine']);
if (!empty($filters['refine_negate'])) {
$sql->where('s.name NOT LIKE \'%' . $refine . '%\'');
} else {
$sql->where('s.name LIKE \'%' . $refine . '%\'');
}
}
if (!empty($filters['active_only'])) {
$sql->where('s.active = 1');
}
return (int) Db::getInstance()->getValue($sql);
}
/**
* Get shops by IDs
*/
public function getTargetShopsByIds(array $ids, $idLang)
{
if (empty($ids)) {
return [];
}
$sql = new DbQuery();
$sql->select('s.id_shop, s.name, s.active');
$sql->from('shop', 's');
$sql->where('s.id_shop IN (' . implode(',', array_map('intval', $ids)) . ')');
$results = Db::getInstance()->executeS($sql);
if (!$results) {
return [];
}
$shops = [];
foreach ($results as $row) {
$shops[(int) $row['id_shop']] = [
'id' => (int) $row['id_shop'],
'type' => 'shop',
'name' => $row['name'],
'active' => (bool) $row['active'],
];
}
$ordered = [];
foreach ($ids as $id) {
if (isset($shops[(int) $id])) {
$ordered[] = $shops[(int) $id];
}
}
return $ordered;
}
// =========================================================================
// PROFILES
// =========================================================================
/**
* Search profiles
*/
public function searchTargetProfiles($query, $idLang, $idShop, array $filters = [])
{
$limit = isset($filters['limit']) ? (int) $filters['limit'] : 20;
$sql = new DbQuery();
$sql->select('DISTINCT p.id_profile, pl.name');
$sql->from('profile', 'p');
$sql->leftJoin('profile_lang', 'pl', 'pl.id_profile = p.id_profile AND pl.id_lang = ' . (int) $idLang);
if (!empty($query)) {
$escapedQuery = $this->escapePattern($query);
$sql->where('(pl.name LIKE \'%' . $escapedQuery . '%\' OR p.id_profile = ' . (int) $query . ')');
}
if (!empty($filters['refine'])) {
$refine = $this->escapePattern($filters['refine']);
if (!empty($filters['refine_negate'])) {
$sql->where('pl.name NOT LIKE \'%' . $refine . '%\'');
} else {
$sql->where('pl.name LIKE \'%' . $refine . '%\'');
}
}
// Sorting
$profileSortMap = [
'name' => 'pl.name',
'id' => 'p.id_profile',
];
$sql->orderBy($this->buildOrderBy('profiles', $filters, $profileSortMap));
$sql->limit($limit);
$results = Db::getInstance()->executeS($sql);
if (!$results) {
return [];
}
return array_map(function ($row) {
return [
'id' => (int) $row['id_profile'],
'type' => 'profile',
'name' => $row['name'],
];
}, $results);
}
/**
* Count profiles
*/
public function countTargetProfiles($query, $idLang, $idShop, array $filters = [])
{
$sql = new DbQuery();
$sql->select('COUNT(DISTINCT p.id_profile)');
$sql->from('profile', 'p');
$sql->leftJoin('profile_lang', 'pl', 'pl.id_profile = p.id_profile AND pl.id_lang = ' . (int) $idLang);
if (!empty($query)) {
$escapedQuery = $this->escapePattern($query);
$sql->where('(pl.name LIKE \'%' . $escapedQuery . '%\' OR p.id_profile = ' . (int) $query . ')');
}
if (!empty($filters['refine'])) {
$refine = $this->escapePattern($filters['refine']);
if (!empty($filters['refine_negate'])) {
$sql->where('pl.name NOT LIKE \'%' . $refine . '%\'');
} else {
$sql->where('pl.name LIKE \'%' . $refine . '%\'');
}
}
return (int) Db::getInstance()->getValue($sql);
}
/**
* Get profiles by IDs
*/
public function getTargetProfilesByIds(array $ids, $idLang)
{
if (empty($ids)) {
return [];
}
$sql = new DbQuery();
$sql->select('p.id_profile, pl.name');
$sql->from('profile', 'p');
$sql->leftJoin('profile_lang', 'pl', 'pl.id_profile = p.id_profile AND pl.id_lang = ' . (int) $idLang);
$sql->where('p.id_profile IN (' . implode(',', array_map('intval', $ids)) . ')');
$results = Db::getInstance()->executeS($sql);
if (!$results) {
return [];
}
$profiles = [];
foreach ($results as $row) {
$profiles[(int) $row['id_profile']] = [
'id' => (int) $row['id_profile'],
'type' => 'profile',
'name' => $row['name'],
];
}
$ordered = [];
foreach ($ids as $id) {
if (isset($profiles[(int) $id])) {
$ordered[] = $profiles[(int) $id];
}
}
return $ordered;
}
// =========================================================================
// ORDER STATES
// =========================================================================
/**
* Search order states
*/
public function searchTargetOrderStates($query, $idLang, $idShop, array $filters = [])
{
$limit = isset($filters['limit']) ? (int) $filters['limit'] : 20;
$sql = new DbQuery();
$sql->select('DISTINCT os.id_order_state, osl.name, os.color, os.paid, os.shipped, os.delivery, os.invoice');
$sql->from('order_state', 'os');
$sql->leftJoin('order_state_lang', 'osl', 'osl.id_order_state = os.id_order_state AND osl.id_lang = ' . (int) $idLang);
$sql->where('os.deleted = 0');
if (!empty($query)) {
$escapedQuery = $this->escapePattern($query);
$sql->where('(osl.name LIKE \'%' . $escapedQuery . '%\' OR os.id_order_state = ' . (int) $query . ')');
}
if (!empty($filters['refine'])) {
$refine = $this->escapePattern($filters['refine']);
if (!empty($filters['refine_negate'])) {
$sql->where('osl.name NOT LIKE \'%' . $refine . '%\'');
} else {
$sql->where('osl.name LIKE \'%' . $refine . '%\'');
}
}
// Sorting
$orderStateSortMap = [
'name' => 'osl.name',
'id' => 'os.id_order_state',
];
$sql->orderBy($this->buildOrderBy('order_states', $filters, $orderStateSortMap));
$sql->limit($limit);
$results = Db::getInstance()->executeS($sql);
if (!$results) {
return [];
}
return array_map(function ($row) {
return [
'id' => (int) $row['id_order_state'],
'type' => 'order_state',
'name' => $row['name'],
'color' => $row['color'],
'paid' => (bool) $row['paid'],
'shipped' => (bool) $row['shipped'],
'delivery' => (bool) $row['delivery'],
'invoice' => (bool) $row['invoice'],
];
}, $results);
}
/**
* Count order states
*/
public function countTargetOrderStates($query, $idLang, $idShop, array $filters = [])
{
$sql = new DbQuery();
$sql->select('COUNT(DISTINCT os.id_order_state)');
$sql->from('order_state', 'os');
$sql->leftJoin('order_state_lang', 'osl', 'osl.id_order_state = os.id_order_state AND osl.id_lang = ' . (int) $idLang);
$sql->where('os.deleted = 0');
if (!empty($query)) {
$escapedQuery = $this->escapePattern($query);
$sql->where('(osl.name LIKE \'%' . $escapedQuery . '%\' OR os.id_order_state = ' . (int) $query . ')');
}
if (!empty($filters['refine'])) {
$refine = $this->escapePattern($filters['refine']);
if (!empty($filters['refine_negate'])) {
$sql->where('osl.name NOT LIKE \'%' . $refine . '%\'');
} else {
$sql->where('osl.name LIKE \'%' . $refine . '%\'');
}
}
return (int) Db::getInstance()->getValue($sql);
}
/**
* Get order states by IDs
*/
public function getTargetOrderStatesByIds(array $ids, $idLang)
{
if (empty($ids)) {
return [];
}
$sql = new DbQuery();
$sql->select('os.id_order_state, osl.name, os.color');
$sql->from('order_state', 'os');
$sql->leftJoin('order_state_lang', 'osl', 'osl.id_order_state = os.id_order_state AND osl.id_lang = ' . (int) $idLang);
$sql->where('os.id_order_state IN (' . implode(',', array_map('intval', $ids)) . ')');
$results = Db::getInstance()->executeS($sql);
if (!$results) {
return [];
}
$states = [];
foreach ($results as $row) {
$states[(int) $row['id_order_state']] = [
'id' => (int) $row['id_order_state'],
'type' => 'order_state',
'name' => $row['name'],
'color' => $row['color'],
];
}
$ordered = [];
foreach ($ids as $id) {
if (isset($states[(int) $id])) {
$ordered[] = $states[(int) $id];
}
}
return $ordered;
}
// =========================================================================
// TAXES
// =========================================================================
/**
* Search taxes
*/
public function searchTargetTaxes($query, $idLang, $idShop, array $filters = [])
{
$limit = isset($filters['limit']) ? (int) $filters['limit'] : 20;
$sql = new DbQuery();
$sql->select('DISTINCT t.id_tax, tl.name, t.rate, t.active');
$sql->from('tax', 't');
$sql->leftJoin('tax_lang', 'tl', 'tl.id_tax = t.id_tax AND tl.id_lang = ' . (int) $idLang);
if (!empty($query)) {
$escapedQuery = $this->escapePattern($query);
$sql->where('(tl.name LIKE \'%' . $escapedQuery . '%\' OR t.id_tax = ' . (int) $query . ')');
}
if (!empty($filters['refine'])) {
$refine = $this->escapePattern($filters['refine']);
if (!empty($filters['refine_negate'])) {
$sql->where('tl.name NOT LIKE \'%' . $refine . '%\'');
} else {
$sql->where('tl.name LIKE \'%' . $refine . '%\'');
}
}
if (!empty($filters['active_only'])) {
$sql->where('t.active = 1');
}
// Sorting
$taxSortMap = [
'name' => 'tl.name',
'id' => 't.id_tax',
'rate' => 't.rate',
];
$sql->orderBy($this->buildOrderBy('taxes', $filters, $taxSortMap));
$sql->limit($limit);
$results = Db::getInstance()->executeS($sql);
if (!$results) {
return [];
}
return array_map(function ($row) {
return [
'id' => (int) $row['id_tax'],
'type' => 'tax',
'name' => $row['name'],
'rate' => (float) $row['rate'],
'active' => (bool) $row['active'],
];
}, $results);
}
/**
* Count taxes
*/
public function countTargetTaxes($query, $idLang, $idShop, array $filters = [])
{
$sql = new DbQuery();
$sql->select('COUNT(DISTINCT t.id_tax)');
$sql->from('tax', 't');
$sql->leftJoin('tax_lang', 'tl', 'tl.id_tax = t.id_tax AND tl.id_lang = ' . (int) $idLang);
if (!empty($query)) {
$escapedQuery = $this->escapePattern($query);
$sql->where('(tl.name LIKE \'%' . $escapedQuery . '%\' OR t.id_tax = ' . (int) $query . ')');
}
if (!empty($filters['refine'])) {
$refine = $this->escapePattern($filters['refine']);
if (!empty($filters['refine_negate'])) {
$sql->where('tl.name NOT LIKE \'%' . $refine . '%\'');
} else {
$sql->where('tl.name LIKE \'%' . $refine . '%\'');
}
}
if (!empty($filters['active_only'])) {
$sql->where('t.active = 1');
}
return (int) Db::getInstance()->getValue($sql);
}
/**
* Get taxes by IDs
*/
public function getTargetTaxesByIds(array $ids, $idLang)
{
if (empty($ids)) {
return [];
}
$sql = new DbQuery();
$sql->select('t.id_tax, tl.name, t.rate, t.active');
$sql->from('tax', 't');
$sql->leftJoin('tax_lang', 'tl', 'tl.id_tax = t.id_tax AND tl.id_lang = ' . (int) $idLang);
$sql->where('t.id_tax IN (' . implode(',', array_map('intval', $ids)) . ')');
$results = Db::getInstance()->executeS($sql);
if (!$results) {
return [];
}
$taxes = [];
foreach ($results as $row) {
$taxes[(int) $row['id_tax']] = [
'id' => (int) $row['id_tax'],
'type' => 'tax',
'name' => $row['name'],
'rate' => (float) $row['rate'],
'active' => (bool) $row['active'],
];
}
$ordered = [];
foreach ($ids as $id) {
if (isset($taxes[(int) $id])) {
$ordered[] = $taxes[(int) $id];
}
}
return $ordered;
}
// =========================================================================
// ATTRIBUTES
// =========================================================================
/**
* Search attributes (attribute values with their groups)
*/
public function searchTargetAttributes($query, $idLang, $idShop, $limit = 20, $offset = 0, array $filters = [])
{
$sql = new DbQuery();
$sql->select('DISTINCT a.id_attribute, al.name, a.color, a.position, a.id_attribute_group');
$sql->select('agl.name AS group_name, ag.is_color_group');
$sql->from('attribute', 'a');
$sql->innerJoin('attribute_shop', 'ash', 'ash.id_attribute = a.id_attribute AND ash.id_shop = ' . (int) $idShop);
$sql->leftJoin('attribute_lang', 'al', 'al.id_attribute = a.id_attribute AND al.id_lang = ' . (int) $idLang);
$sql->leftJoin('attribute_group', 'ag', 'ag.id_attribute_group = a.id_attribute_group');
$sql->leftJoin('attribute_group_lang', 'agl', 'agl.id_attribute_group = a.id_attribute_group AND agl.id_lang = ' . (int) $idLang);
if (!empty($query)) {
$escapedQuery = $this->escapePattern($query);
$sql->where('(al.name LIKE \'%' . $escapedQuery . '%\' OR agl.name LIKE \'%' . $escapedQuery . '%\' OR a.id_attribute = ' . (int) $query . ')');
}
if (!empty($filters['refine'])) {
$refine = $this->escapePattern($filters['refine']);
if (!empty($filters['refine_negate'])) {
$sql->where('al.name NOT LIKE \'%' . $refine . '%\'');
} else {
$sql->where('al.name LIKE \'%' . $refine . '%\'');
}
}
// Filter by attribute group
if ($filters['attribute_group'] !== null) {
$sql->where('a.id_attribute_group = ' . (int) $filters['attribute_group']);
}
// Filter by color
if (!empty($filters['is_color'])) {
$sql->where('ag.is_color_group = 1');
}
// Sorting
$attributeSortMap = [
'name' => 'al.name',
'id' => 'a.id_attribute',
'position' => 'a.position',
'group' => 'agl.name',
];
$sql->orderBy($this->buildOrderBy('attributes', $filters, $attributeSortMap));
$sql->limit((int) $limit, (int) $offset);
$results = Db::getInstance()->executeS($sql);
if (!$results) {
return [];
}
return array_map(function ($row) {
return [
'id' => (int) $row['id_attribute'],
'type' => 'attribute',
'name' => $row['name'],
'color' => $row['color'],
'position' => (int) $row['position'],
'id_attribute_group' => (int) $row['id_attribute_group'],
'group_name' => $row['group_name'],
'is_color_group' => (bool) $row['is_color_group'],
];
}, $results);
}
/**
* Count attributes
*/
public function countTargetAttributes($query, $idLang, $idShop, array $filters = [])
{
$sql = new DbQuery();
$sql->select('COUNT(DISTINCT a.id_attribute)');
$sql->from('attribute', 'a');
$sql->innerJoin('attribute_shop', 'ash', 'ash.id_attribute = a.id_attribute AND ash.id_shop = ' . (int) $idShop);
$sql->leftJoin('attribute_lang', 'al', 'al.id_attribute = a.id_attribute AND al.id_lang = ' . (int) $idLang);
$sql->leftJoin('attribute_group', 'ag', 'ag.id_attribute_group = a.id_attribute_group');
$sql->leftJoin('attribute_group_lang', 'agl', 'agl.id_attribute_group = a.id_attribute_group AND agl.id_lang = ' . (int) $idLang);
if (!empty($query)) {
$escapedQuery = $this->escapePattern($query);
$sql->where('(al.name LIKE \'%' . $escapedQuery . '%\' OR agl.name LIKE \'%' . $escapedQuery . '%\' OR a.id_attribute = ' . (int) $query . ')');
}
if (!empty($filters['refine'])) {
$refine = $this->escapePattern($filters['refine']);
if (!empty($filters['refine_negate'])) {
$sql->where('al.name NOT LIKE \'%' . $refine . '%\'');
} else {
$sql->where('al.name LIKE \'%' . $refine . '%\'');
}
}
if ($filters['attribute_group'] !== null) {
$sql->where('a.id_attribute_group = ' . (int) $filters['attribute_group']);
}
if (!empty($filters['is_color'])) {
$sql->where('ag.is_color_group = 1');
}
return (int) Db::getInstance()->getValue($sql);
}
/**
* Get attributes by IDs
*/
public function getTargetAttributesByIds(array $ids, $idLang)
{
if (empty($ids)) {
return [];
}
$sql = new DbQuery();
$sql->select('a.id_attribute, al.name, a.color, a.id_attribute_group, agl.name AS group_name');
$sql->from('attribute', 'a');
$sql->leftJoin('attribute_lang', 'al', 'al.id_attribute = a.id_attribute AND al.id_lang = ' . (int) $idLang);
$sql->leftJoin('attribute_group_lang', 'agl', 'agl.id_attribute_group = a.id_attribute_group AND agl.id_lang = ' . (int) $idLang);
$sql->where('a.id_attribute IN (' . implode(',', array_map('intval', $ids)) . ')');
$results = Db::getInstance()->executeS($sql);
if (!$results) {
return [];
}
$attributes = [];
foreach ($results as $row) {
$attributes[(int) $row['id_attribute']] = [
'id' => (int) $row['id_attribute'],
'type' => 'attribute',
'name' => $row['name'],
'color' => $row['color'],
'group_name' => $row['group_name'],
];
}
$ordered = [];
foreach ($ids as $id) {
if (isset($attributes[(int) $id])) {
$ordered[] = $attributes[(int) $id];
}
}
return $ordered;
}
// =========================================================================
// FEATURES
// =========================================================================
/**
* Search features (feature values with their groups)
*/
public function searchTargetFeatures($query, $idLang, $idShop, $limit = 20, $offset = 0, array $filters = [])
{
$sql = new DbQuery();
$sql->select('DISTINCT fv.id_feature_value, fvl.value AS name, fv.custom, fv.id_feature');
$sql->select('fl.name AS feature_name');
$sql->from('feature_value', 'fv');
$sql->innerJoin('feature_shop', 'fsh', 'fsh.id_feature = fv.id_feature AND fsh.id_shop = ' . (int) $idShop);
$sql->leftJoin('feature_value_lang', 'fvl', 'fvl.id_feature_value = fv.id_feature_value AND fvl.id_lang = ' . (int) $idLang);
$sql->leftJoin('feature_lang', 'fl', 'fl.id_feature = fv.id_feature AND fl.id_lang = ' . (int) $idLang);
if (!empty($query)) {
$escapedQuery = $this->escapePattern($query);
$sql->where('(fvl.value LIKE \'%' . $escapedQuery . '%\' OR fl.name LIKE \'%' . $escapedQuery . '%\' OR fv.id_feature_value = ' . (int) $query . ')');
}
if (!empty($filters['refine'])) {
$refine = $this->escapePattern($filters['refine']);
if (!empty($filters['refine_negate'])) {
$sql->where('fvl.value NOT LIKE \'%' . $refine . '%\'');
} else {
$sql->where('fvl.value LIKE \'%' . $refine . '%\'');
}
}
// Filter by feature group
if ($filters['feature_group'] !== null) {
$sql->where('fv.id_feature = ' . (int) $filters['feature_group']);
}
// Exclude custom values
if (!empty($filters['is_custom'])) {
$sql->where('fv.custom = 1');
}
// Sorting
$featureSortMap = [
'name' => 'fvl.value',
'id' => 'fv.id_feature_value',
'feature' => 'fl.name',
];
$sql->orderBy($this->buildOrderBy('features', $filters, $featureSortMap));
$sql->limit((int) $limit, (int) $offset);
$results = Db::getInstance()->executeS($sql);
if (!$results) {
return [];
}
return array_map(function ($row) {
return [
'id' => (int) $row['id_feature_value'],
'type' => 'feature',
'name' => $row['name'],
'custom' => (bool) $row['custom'],
'id_feature' => (int) $row['id_feature'],
'feature_name' => $row['feature_name'],
];
}, $results);
}
/**
* Count features
*/
public function countTargetFeatures($query, $idLang, $idShop, array $filters = [])
{
$sql = new DbQuery();
$sql->select('COUNT(DISTINCT fv.id_feature_value)');
$sql->from('feature_value', 'fv');
$sql->innerJoin('feature_shop', 'fsh', 'fsh.id_feature = fv.id_feature AND fsh.id_shop = ' . (int) $idShop);
$sql->leftJoin('feature_value_lang', 'fvl', 'fvl.id_feature_value = fv.id_feature_value AND fvl.id_lang = ' . (int) $idLang);
$sql->leftJoin('feature_lang', 'fl', 'fl.id_feature = fv.id_feature AND fl.id_lang = ' . (int) $idLang);
if (!empty($query)) {
$escapedQuery = $this->escapePattern($query);
$sql->where('(fvl.value LIKE \'%' . $escapedQuery . '%\' OR fl.name LIKE \'%' . $escapedQuery . '%\' OR fv.id_feature_value = ' . (int) $query . ')');
}
if (!empty($filters['refine'])) {
$refine = $this->escapePattern($filters['refine']);
if (!empty($filters['refine_negate'])) {
$sql->where('fvl.value NOT LIKE \'%' . $refine . '%\'');
} else {
$sql->where('fvl.value LIKE \'%' . $refine . '%\'');
}
}
if ($filters['feature_group'] !== null) {
$sql->where('fv.id_feature = ' . (int) $filters['feature_group']);
}
if (!empty($filters['is_custom'])) {
$sql->where('fv.custom = 1');
}
return (int) Db::getInstance()->getValue($sql);
}
/**
* Get features by IDs
*/
public function getTargetFeaturesByIds(array $ids, $idLang)
{
if (empty($ids)) {
return [];
}
$sql = new DbQuery();
$sql->select('fv.id_feature_value, fvl.value AS name, fv.id_feature, fl.name AS feature_name');
$sql->from('feature_value', 'fv');
$sql->leftJoin('feature_value_lang', 'fvl', 'fvl.id_feature_value = fv.id_feature_value AND fvl.id_lang = ' . (int) $idLang);
$sql->leftJoin('feature_lang', 'fl', 'fl.id_feature = fv.id_feature AND fl.id_lang = ' . (int) $idLang);
$sql->where('fv.id_feature_value IN (' . implode(',', array_map('intval', $ids)) . ')');
$results = Db::getInstance()->executeS($sql);
if (!$results) {
return [];
}
$features = [];
foreach ($results as $row) {
$features[(int) $row['id_feature_value']] = [
'id' => (int) $row['id_feature_value'],
'type' => 'feature',
'name' => $row['name'],
'feature_name' => $row['feature_name'],
];
}
$ordered = [];
foreach ($ids as $id) {
if (isset($features[(int) $id])) {
$ordered[] = $features[(int) $id];
}
}
return $ordered;
}
// =========================================================================
// TAGS
// =========================================================================
/**
* Search tags
*/
public function searchTargetTags($query, $idLang, $idShop, array $filters = [])
{
$limit = isset($filters['limit']) ? (int) $filters['limit'] : 20;
$sql = new DbQuery();
$sql->select('DISTINCT t.id_tag, t.name, t.id_lang');
$sql->select('(SELECT COUNT(pt.id_product) FROM ' . _DB_PREFIX_ . 'product_tag pt WHERE pt.id_tag = t.id_tag) AS product_count');
$sql->from('tag', 't');
// By default, filter by current language
$sql->where('t.id_lang = ' . (int) $idLang);
if (!empty($query)) {
$escapedQuery = $this->escapePattern($query);
$sql->where('(t.name LIKE \'%' . $escapedQuery . '%\' OR t.id_tag = ' . (int) $query . ')');
}
if (!empty($filters['refine'])) {
$refine = $this->escapePattern($filters['refine']);
if (!empty($filters['refine_negate'])) {
$sql->where('t.name NOT LIKE \'%' . $refine . '%\'');
} else {
$sql->where('t.name LIKE \'%' . $refine . '%\'');
}
}
// Sorting
$tagSortMap = [
'name' => 't.name',
'id' => 't.id_tag',
'popularity' => 'product_count',
];
$sql->orderBy($this->buildOrderBy('tags', $filters, $tagSortMap));
$sql->limit($limit);
$results = Db::getInstance()->executeS($sql);
if (!$results) {
return [];
}
return array_map(function ($row) {
return [
'id' => (int) $row['id_tag'],
'type' => 'tag',
'name' => $row['name'],
'product_count' => (int) $row['product_count'],
];
}, $results);
}
/**
* Count tags
*/
public function countTargetTags($query, $idLang, $idShop, array $filters = [])
{
$sql = new DbQuery();
$sql->select('COUNT(DISTINCT t.id_tag)');
$sql->from('tag', 't');
$sql->where('t.id_lang = ' . (int) $idLang);
if (!empty($query)) {
$escapedQuery = $this->escapePattern($query);
$sql->where('(t.name LIKE \'%' . $escapedQuery . '%\' OR t.id_tag = ' . (int) $query . ')');
}
if (!empty($filters['refine'])) {
$refine = $this->escapePattern($filters['refine']);
if (!empty($filters['refine_negate'])) {
$sql->where('t.name NOT LIKE \'%' . $refine . '%\'');
} else {
$sql->where('t.name LIKE \'%' . $refine . '%\'');
}
}
return (int) Db::getInstance()->getValue($sql);
}
/**
* Get tags by IDs
*/
public function getTargetTagsByIds(array $ids, $idLang)
{
if (empty($ids)) {
return [];
}
$sql = new DbQuery();
$sql->select('t.id_tag, t.name');
$sql->from('tag', 't');
$sql->where('t.id_tag IN (' . implode(',', array_map('intval', $ids)) . ')');
$results = Db::getInstance()->executeS($sql);
if (!$results) {
return [];
}
$tags = [];
foreach ($results as $row) {
$tags[(int) $row['id_tag']] = [
'id' => (int) $row['id_tag'],
'type' => 'tag',
'name' => $row['name'],
];
}
$ordered = [];
foreach ($ids as $id) {
if (isset($tags[(int) $id])) {
$ordered[] = $tags[(int) $id];
}
}
return $ordered;
}
// =========================================================================
// FILTERABLE DATA (for UI dropdowns)
// =========================================================================
/**
* Get attribute groups with their values for filter dropdown
*
* @param int $idLang Language ID
* @param int $idShop Shop ID
* @return array Attribute groups with values
*/
public function getFilterableAttributes($idLang, $idShop)
{
$sql = new DbQuery();
$sql->select('ag.id_attribute_group, agl.name AS group_name, ag.is_color_group');
$sql->from('attribute_group', 'ag');
$sql->innerJoin('attribute_group_shop', 'ags', 'ags.id_attribute_group = ag.id_attribute_group AND ags.id_shop = ' . (int) $idShop);
$sql->leftJoin('attribute_group_lang', 'agl', 'agl.id_attribute_group = ag.id_attribute_group AND agl.id_lang = ' . (int) $idLang);
$sql->orderBy('ag.position ASC');
$groups = Db::getInstance()->executeS($sql);
if (!$groups) {
return [];
}
$result = [];
foreach ($groups as $group) {
$sqlValues = new DbQuery();
$sqlValues->select('a.id_attribute, al.name, a.color');
$sqlValues->from('attribute', 'a');
$sqlValues->innerJoin('attribute_shop', 'ash', 'ash.id_attribute = a.id_attribute AND ash.id_shop = ' . (int) $idShop);
$sqlValues->leftJoin('attribute_lang', 'al', 'al.id_attribute = a.id_attribute AND al.id_lang = ' . (int) $idLang);
$sqlValues->where('a.id_attribute_group = ' . (int) $group['id_attribute_group']);
$sqlValues->orderBy('a.position ASC');
$values = Db::getInstance()->executeS($sqlValues);
$result[] = [
'id' => (int) $group['id_attribute_group'],
'name' => $group['group_name'],
'is_color' => (bool) $group['is_color_group'],
'values' => $values ?: [],
];
}
return $result;
}
/**
* Get features with their values for filter dropdown
*
* @param int $idLang Language ID
* @param int $idShop Shop ID
* @return array Features with values
*/
public function getFilterableFeatures($idLang, $idShop)
{
$sql = new DbQuery();
$sql->select('f.id_feature, fl.name AS feature_name');
$sql->from('feature', 'f');
$sql->innerJoin('feature_shop', 'fs', 'fs.id_feature = f.id_feature AND fs.id_shop = ' . (int) $idShop);
$sql->leftJoin('feature_lang', 'fl', 'fl.id_feature = f.id_feature AND fl.id_lang = ' . (int) $idLang);
$sql->orderBy('f.position ASC');
$features = Db::getInstance()->executeS($sql);
if (!$features) {
return [];
}
$result = [];
foreach ($features as $feature) {
$sqlValues = new DbQuery();
$sqlValues->select('fv.id_feature_value, fvl.value AS name');
$sqlValues->from('feature_value', 'fv');
$sqlValues->leftJoin('feature_value_lang', 'fvl', 'fvl.id_feature_value = fv.id_feature_value AND fvl.id_lang = ' . (int) $idLang);
$sqlValues->where('fv.id_feature = ' . (int) $feature['id_feature']);
$sqlValues->where('fv.custom = 0');
$sqlValues->orderBy('fvl.value ASC');
$values = Db::getInstance()->executeS($sqlValues);
$result[] = [
'id' => (int) $feature['id_feature'],
'name' => $feature['feature_name'],
'values' => $values ?: [],
];
}
return $result;
}
}