Мой блог

Тестирование дизайна в simpla

Задача

Требуется протестировать дизайн на рабочем сайте с Simpla CMS.

Решение

  • Создаете шаблон в папаке /design/
  • Добавляем в файл /api/Design.php код
// Берем тему из настроек
$theme = $this->settings->theme; // после строки добавляем

if(!empty($_GET['theme'])) {
    $theme = $_GET['theme'];
}
  • новый шаблон доступен по адресу http://{SERVER_NAME}/?theme={TEMPLATE_NAME}, где
    {SERVER_NAME} — имя домена;
    {TEMPLATE_NAME} — имя папки шаблона.
2017   simpla

Раздел sale в simpla

Требуется создать категорию, куда будут попадать товары с скидкой.

  • создаем категорию с адресом страницы «sale»
  • в файле /view/ProductsView.php правим код:
// Выберем текущую категорию
if (!empty($category_url))
{
	$category = $this->categories->get_category((string)$category_url);
	if (empty($category) || (!$category->visible && empty($_SESSION['admin'])))
		return false;
	$this->design->assign('category', $category);
	
	if($category->url == 'sale') {
		$filter['discounted'] = 1;
	}
	else {
		$filter['category_id'] = $category->children;
	}
}
  • В файде /api/Products.php обновляем методы get_products и count_products:
public function get_products($filter = array())
{
...
$is_featured_filter = '';
$discounted_filter = '';
$in_stock_filter = '';
...
if(isset($filter['discounted']))
	$discounted_filter = $this->db->placehold('AND (SELECT 1 FROM __variants pv WHERE pv.product_id=p.id AND pv.compare_price>0 LIMIT 1) = ?', intval($filter['discounted']));
...
$query = "SELECT 
...
WHERE
...
$discounted_filter
...
";
...
}
public function count_products($filter = array())
{
...
$is_featured_filter = '';
$discounted_filter = '';
$in_stock_filter = '';
...
if(isset($filter['discounted']))
	$discounted_filter = $this->db->placehold('AND (SELECT 1 FROM __variants pv WHERE pv.product_id=p.id AND pv.compare_price>0 LIMIT 1) = ?', intval($filter['discounted']));
...
$query = "SELECT 
...
WHERE
...
$discounted_filter
...
";
...
}
2016   simpla

Перевод письма в html

Требуется получить корректный html письма для внедрения его на сайте.

  • создал письмо с помощью конструктора писем, на сервисе email-рассылок;
  • отправил тестовое письмо на свой почтовый ящик @gmail.com;
  • скопировал исходный код письма из gmail;
  • убрал «=» из кода в Sublime (заменил символ (\=$\s) на пустоту);
  • декодировал Quoted-printable с помощью сервиса.
2016   html   mail   sublime

Хочу html страницы из sketch

Решил провести тест сервисов/студий, которые переводят sketch файлы в html.

В моём случае, это 3 страницы: главная, результат поиска+форма поиска, карточка детально.

Макеты есть под следующие разрешения: 1280, 1024, 768, 320 пикселей.

Победитель назначил цену = X. Срок реализации 10 рыбочих дней.

Список участников:

  1. http://topfloat.ru
  2. http://csssr.ru
  3. http://coderiver.com.ua
  4. https://www.psd2html.com
  5. https://thesiteslinger.com
  6. https://www.xfive.co
  7. http://www.htmlpanda.com

Отклик на заявку

http://topfloat.ru

Время: без оценки. С ребятами мы уже делали проект, потому отклик на заявку был моментальным.
Вопросы по заявке: данная компания умеет декомпозировать задачи любых объемов и вникать в суть проекта, поэтому задачи выполняются в срок

http://csssr.ru

Время: 30 минут.
Вопросы по заявке: вопросы по делу.

http://coderiver.com.ua

Время: 4 часа 30 минут.
Вопросы по заявке: без уточнений, выдали сразу расчет.

https://thesiteslinger.com

Время: 1 часа 20 минут. На сайте интересно построено взаимодействие, для заказчика заводится профиль и ведется общение через внутреннюю переписку.
Вопросы по заявке: сначала общается менеджер по заявке, после идет уточнение у разработчиков.

https://www.xfive.co

Время: 12 часов.
Вопросы по заявке: вопросы были направлены на проект, ожидаемый результат, технические решения, предложения по дополнительным работам.

https://www.psd2html.com

Время: сошли с дистанции

http://www.htmlpanda.com

Время: сошли с дистанции

Цена, сроки

https://www.xfive.co

Спустя 4 часа, с момента последнего письма, получит расчет. В расчете описан рабочий процесс от общения с менеджером до реализации макетов. Отдельно описаны доп работы по проекту, сроки, когда команда может приступить к реализации, срок действия гарантии.
Цена: X
Время: 38 hours

http://topfloat.ru

Через 2 дня я получил декомпозированный проект, с минимальным шагом в 1 час работ.
Цена: 1.28 * X
Время: 114 часов

http://coderiver.com.ua

Цена: 0.25 * X
Время: 40 часов

http://csssr.ru

Итоговое письмо содержало странные фразы «Комфортный для нас срок...», «стоимость будет варьироваться в пределе...».
В вопросе технологий предлагаю выступить по классике и без необходимости не прибегать к использованию фреймворков.
Цена: 3.8 * X
Время: 15 дней

https://thesiteslinger.com

Через день я получил стоимость верстки каждого экрана.
Цена: 1.50 * X
Время: 10 рабочих дней

2016   sketch

sitemap для Эгеи

Решил ковырнуть движок, написать компактно и вот что у меня получилось:

<?
require 'system/core.php';

$domain = d83c8('e2m_frontpage'); // в функции есть строка if (@$_config['preferred_domain_name'] and $_SERVER['HTTP_HOST'] != $_config['preferred_domain_name']) {
$dir = 'blog'; // Если блог находится в папке

if (r0f7c()) { // в функции есть строка global $settings, $_db, $_db_error;
	$xml = '<?xml version="1.0" encoding="UTF-8"?>';
	$xml .= '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">';

	$xml .= "
	<url>
	        <loc>$domain</loc>
	        <priority>1.0</priority>
	</url>\n";

	$result = mysql_query("SELECT OriginalAlias, LastModified FROM `e2_Notes` WHERE
								IsPublished = 1
								AND IsIssue = 0
								AND isvisible = 1");
	while ($data = mysql_fetch_array($result)) {
		$page = d83c8('e2m_note', array('alias' => $data['OriginalAlias']));
		$xml .= "
	<url>
	        <loc>".str_replace('?go=', $dir.'/', $page)."</loc>
	        <priority>0.5</priority>
	        <lastmod>".date('Y-m-d', $data['LastModified'])."</lastmod>
	</url>\n";
	}
	$xml .= "
	</urlset>";
	$fp = fopen(ROOT_FOLDER.'sitemap.xml', 'w'); // здесь укажите свой путь к файлу sitemap.xml
	fwrite($fp, $xml);
	fclose($fp);
}

добавить задание cron.

2016   e2   sitemap

Simpla vs. WebAsyst

Очень долго у Simpla CMS не было равных по юзабилити, простоте интеграции и бизнес-процессу выстроенному в бэкофисе.

Недавно услышал про WebAsyst и их модуле интернет-магазина. Демо режим данной CMS мне понравился, ребята проделали большой объем работ и им удалось не только повторить функционал Simpla, но и сделать несколько хороших доработок.

Появился клиент с которым захотел попробовать WebAsyst. Последовательность стандартная:

  1. определяем функционал
  2. выбираем шаблон
  3. интегрируем

Послевкусие

  • Наличие у WebAsyst готовых решений, сильно упрощает задачу по доработке нового функционала, часто он уже есть и стоит вменяемых денег.
  • Если вы приобретаете шаблон не у WebAsyst, вас жду сюрпризы. Дело в том, что не все элементы страницы имеют tpl файлы. Например, страница оформление заказа спрятана очень глубоко и править ей придется дописывая CSS классы под уже имеющуюся форму.
  • Если попытаешься засучить рукава и «быстро поправить», знай, все будет очень не просто, придется долго читать мануалы и копаться в структуре, чтобы добраться до нужного куска кода.
  • Ответы в комьюнити часто упираются в то, что так делать нельзя. Просто потому что реализация глубоко закопана и это не предусмотрели для модификации. С одной стороны это защищает код от кривых рук, с другой стороны нельзя немного допилить систему под свои задачи.
  • Наличие документации упрощает время погружения в систему.
2016   simpla   webasyst

Доработка раздела пользователей в Simpla

Когда постоянных покупателей становится много, появляется задача по их сегментации.

В моём случае потребовалось:

  • отобразить сумму покупок
  • реализовать сортировку по сумме покупок

/api/Users.php

switch ($filter['sort'])
{
    ...
    case 'top_price':
        $order = 'o.total_price DESC';
        break;
}
...
$query = $this->db->placehold("SELECT
    u.id, u.email, u.name, u.password, u.address, u.phone, u.group_id, u.enabled, u.last_ip, u.created, g.discount, g.name as group_name, o.total_price FROM __users u
    LEFT JOIN __groups g ON u.group_id=g.id 
    LEFT JOIN (SELECT
                SUM(total_price) total_price, user_id FROM __orders
                WHERE paid = 1 AND closed = 1 AND user_id <> 0 group by user_id) o
    ON o.user_id = u.id
    WHERE 1 $group_id_filter $keyword_filter $name_empty_filter ORDER BY $order $sql_limit");

/simpla/design/html/users.tpl

{if $sort!='name'}<a href="{url sort=name}">имени</a>{else}имени{/if} или
{if $sort!='top_price'}<a href="{url sort=top_price}">сумме покупок</a>{else}сумме покупок{/if} или
...
<div class="price cell">
    {if $user->total_price}{$user->total_price} {$currency->sign}{/if}
</div>
<div class="user_group cell">
    {$groups[$user->group_id]->name}
</div>

/simpla/design/css/style.css

#list .user_group {
    width:150px;
    text-align: right;
    color: #707070;
}
#list .price {
    width:65px;
    text-align: left;
    color: #707070;
}

Импорт товаров в Simpla, загрузка фото по url

Потребовалось реализовать загрузку фото товаров, если указана прямая ссылка на источник.

Как мы знаем, при импорте можно указать фото товара через запятую в поле images или изображения.

Добавим метод в /api/Image.php

public function download_image_from_site($url, $type = 'copy') {
    if(strpos($url, 'http://') === false)
        return false;
    
    if($type == 'copy') {
        $name = $this->correct_filename(pathinfo($url, PATHINFO_BASENAME));
        $base = pathinfo($url, PATHINFO_FILENAME);
        $ext = pathinfo($url, PATHINFO_EXTENSION);
        
        if(in_array(strtolower($ext), $this->allowed_extentions))
        {
            while(file_exists($this->config->root_dir.$this->config->original_images_dir.$name))
            {    
                $name = pathinfo($name, PATHINFO_FILENAME);
                if(preg_match('/_([0-9]+)$/', $name, $parts))
                    $name = $base.'_'.($parts[1]+1).'.'.$ext;
                else
                    $name = $base.'_1.'.$ext;
            }
            if(copy($url, $this->config->root_dir.$this->config->original_images_dir.$name))
                return $name;
        }
        return false;
    }

    return false;
}

Добавим данный метод в /simpla/ajax/import.php. Добавим код

if(strpos($image, 'http://') !== false) {
    $image = $this->image->download_image_from_site($image);
}

// Имя файла
$image_filename = pathinfo($image, PATHINFO_BASENAME);

Комплект в Simpla

Рассмотри задачу продажи товаров в комплекте.

Комплект состоит из товаров каталога. Выгода заключается в том, что цена комплекта ниже, чем сумма цен товаров купленных отдельно.

Рассмотрим реализацию в Simpla CMS

Реализация состоит в копировании функционала вариантов и сопуствующих товаров.

Нам потребуется:

  1. Добавить таблицу комлекта и изменить таблицу products;
  2. Доработать api товаров и добавить методы для работы с комплектами;
  3. Доработать админку для работы с комплектами;
  4. Сверстать отображение.
  5. Изменить оформление заказа
  6. Обновить автокомплиттер

Создадим таблицу s_products_kit и обновим s_products

CREATE TABLE IF NOT EXISTS `s_products_kit` (
  `id` int(11) NOT NULL,
  `main_product_id` int(11) NOT NULL DEFAULT '0',
  `product_id` int(11) DEFAULT '0',
  `variant_id` int(11) DEFAULT NULL,
  `product_name` varchar(255) NOT NULL DEFAULT '',
  `variant_name` varchar(255) NOT NULL,
  `price` float(10,2) NOT NULL DEFAULT '0.00',
  `price_old` float(10,2) NOT NULL,
  `amount` int(11) NOT NULL DEFAULT '0',
  `position` int(11) NOT NULL,
  `sku` varchar(255) NOT NULL
) ENGINE=MyISAM AUTO_INCREMENT=14 DEFAULT CHARSET=utf8;

ALTER TABLE `s_products_kit`
  ADD PRIMARY KEY (`id`),
  ADD KEY `product_id` (`main_product_id`);
  MODIFY `id` int(11) NOT NULL AUTO_INCREMENT,AUTO_INCREMENT=20;

ALTER TABLE `s_products`
  ADD COLUMN `kit` TINYINT(1) SET DEFAULT 0;

Дополним api

В файле /api/Products.php добавим методы для работы с комплектами:

public function get_products_kit($filter = array())
{
    $product_id_filter = '';
    
    if(!empty($filter['main_product_id']))
        $product_id_filter = $this->db->placehold('AND v.main_product_id in(?@)', (array)$filter['main_product_id']);
    
    if(!empty($filter['id']))
        $variant_id_filter = $this->db->placehold('AND v.id in(?@)', (array)$filter['id']);

    if(!$product_id_filter)
        return array();
    
    $query = $this->db->placehold("SELECT *
                FROM __products_kit AS v
                WHERE 
                1
                $product_id_filter
                ORDER BY v.position
                ");
    
    $this->db->query($query);    
    return $this->db->results();
}

public function get_product_kit($id)
{
    if(empty($id))
        return false;
        
    $query = $this->db->placehold("SELECT * FROM __products_kit WHERE id=?", $id);
    
    $this->db->query($query);    
    $product_kit = $this->db->result();
    return $product_kit;
}

public function update_product_kit($id, $product_kit)
{
    $query = $this->db->placehold("UPDATE __products_kit SET ?% WHERE id=? LIMIT 1", $product_kit, intval($id));
    $this->db->query($query);
    return $id;
}

public function add_product_kit($product_kit)
{
    $query = $this->db->placehold("INSERT INTO __products_kit SET ?%", $product_kit);
    $this->db->query($query);
    return $this->db->insert_id();
}

public function delete_product_kit($id)
{
    if(!empty($id))
    {
        $query = $this->db->placehold("DELETE FROM __products_kit WHERE id = ? LIMIT 1", intval($id));
        $this->db->query($query);
    }
}

и изменим уже имеющиеся:

public function get_products($filter = array())
{
    ...
    $visible_filter = '';
    $is_featured_filter = '';
    $is_kit_filter = ''; //выбираем комплекты
    ...
    if(isset($filter['featured']))
        $is_featured_filter = $this->db->placehold('AND p.featured=?', intval($filter['featured']));

    if(isset($filter['kit']))
        $is_kit_filter = $this->db->placehold('AND p.kit=?', intval($filter['kit']));
    ...

    $query = "SELECT  
                    p.id,
                    p.url,
                    p.brand_id,
                    p.name,
                    p.annotation,
                    p.body,
                    p.position,
                    p.created as created,
                    p.visible, 
                    p.featured,
                    p.meta_title, 
                    p.meta_keywords, 
                    p.meta_description, 
                    b.name as brand,
                    b.url as brand_url,
                    cpp.url as kit_url,
                    cpp.name as kit_name,
                    cpp.id as kit_id
                FROM __products p       
                $category_id_filter 
                LEFT JOIN __brands b ON p.brand_id = b.id
                LEFT JOIN __products_kit cp ON cp.main_product_id = p.id
                LEFT JOIN __products cpp ON cp.product_id = cpp.id
                WHERE 
                    1
                    $product_id_filter
                    $brand_id_filter
                    $features_filter
                    $keyword_filter
                    $is_featured_filter
                    $is_kit_filter
                    $discounted_filter
                    $in_stock_filter
                    $visible_filter
                $group_by
                ORDER BY $order
                    $sql_limit";
    ...
}

public function count_products($filter = array())
{
    ...
    $is_featured_filter = '';
    $is_kit_filter = '';
    ...
    if(isset($filter['featured']))
        $is_featured_filter = $this->db->placehold('AND p.featured=?', intval($filter['featured']));
        
    if(isset($filter['kit']))
        $is_kit_filter = $this->db->placehold('AND p.kit=?', intval($filter['kit']));
    ...
    $query = "SELECT count(distinct p.id) as count
                FROM __products AS p
                $category_id_filter
                WHERE 1
                    $brand_id_filter
                    $product_id_filter
                    $keyword_filter
                    $is_featured_filter
                    $is_kit_filter
                    $in_stock_filter
                    $discounted_filter
                    $visible_filter
                    $features_filter ";
    ...
}

public function get_product($id)
{
    ...
    $query = "SELECT DISTINCT
                    p.id,
                    p.url,
                    p.brand_id,
                    p.name,
                    p.annotation,
                    p.body,
                    p.position,
                    p.created as created,
                    p.visible, 
                    p.featured,
                    p.kit,
                    p.meta_title, 
                    p.meta_keywords, 
                    p.meta_description,
                    cpp.url as kit_url,
                    cpp.name as kit_name,
                    cpp.id as kit_id
                FROM __products AS p
                LEFT JOIN __products_kit cp ON cp.main_product_id = p.id
                LEFT JOIN __products cpp ON cp.product_id = cpp.id
                WHERE $filter
                GROUP BY p.id
                LIMIT 1";
        $this->db->query($query);
        $product = $this->db->result();
        return $product;
    ...
}

public function delete_product($id)
{
    ...
    // Удаляем товар из связанных с другими
    $query = $this->db->placehold("DELETE FROM __related_products WHERE related_id=?", intval($id));
    $this->db->query($query);

    // Удаляем товары коллекции
    $collection = $this->get_collection_products($id);
    foreach($collection as $c)
        $this->delete_collection_product($id, $c->related_id);
    // Удаляем товар из коллекции с другими
    $query = $this->db->placehold("DELETE FROM __products_collection WHERE related_id=?", intval($id));
    $this->db->query($query);
    ...
}

public function duplicate_product($id)
{
    ...
    $product->created = null;
    unset($product->kit_url);
    unset($product->kit_name);
    unset($product->kit_id);
    ...
}

Поправим админку

Внесем правки в файл /simpla/ProductAdmin.php:

...
    // Связанные товары
    if(is_array($this->request->post('related_products')))
    {
        foreach($this->request->post('related_products') as $p)
        {
            $rp[$p] = new stdClass;
            $rp[$p]->product_id = $product->id;
            $rp[$p]->related_id = $p;
        }
        $related_products = $rp;
    }

    $product->kit = 0;
    // Товары комплект
    $kit = $this->request->post('kit');
    if(!empty($kit))
    {
        $product->kit = 1;// Товар является комплектом
        
        if(is_array($this->request->post('kit')))
        {
            foreach($this->request->post('kit') as $n=>$ki)
            {
                foreach($ki as $i=>$v)
                {
                    if(empty($kit[$i]))
                        $kit[$i] = new stdClass;
                    $kit_temp[$i]->$n = $v;
                }
            }
        }
    }
    $kit = $kit_temp;
    ...
    // Связанные товары
    $query = $this->db->placehold('DELETE FROM __related_products WHERE product_id=?', $product->id);
    $this->db->query($query);
        if(is_array($related_products))
    {
        $pos = 0;
        foreach($related_products  as $i=>$related_product)
            $this->products->add_related_product($product->id, $related_product->related_id, $pos++);
    }
    // Комплект
    if(!empty($kit))
    { 
        $kit_ids = array();
        foreach($kit as $index=>&$kit_product)
        {
            if(!empty($kit_product->id))
                $this->products->update_product_kit($kit_product->id, $kit_product);
            else
            {
                $kit_product->main_product_id = $product->id;
                $kit_product->id = $this->products->add_product_kit($kit_product);
            }
            $kit_product = $this->products->get_product_kit($kit_product->id);
            if(!empty($kit_product->id))
                $kit_product_ids[] = $kit_product->id;
        }
        
        // Удалить непереданные товары комплекта
        $current_products_kit = $this->products->get_products_kit(array('main_product_id'=>$product->id));
        foreach($current_products_kit as $current_product_kit)
            if(!in_array($current_product_kit->id, $kit_product_ids))
                $this->products->delete_product_kit($current_product_kit->id);
        
        // Отсортировать варианты
        asort($kit_product_ids);
        $i = 0;
        foreach($kit_product_ids as $kit_id)
        {
            $this->products->update_product_kit($kit_product_ids[$i], array('position'=>$kit_id));
            $i++;
        }
    }
    ...
    if(is_array($options))
    {
        ...
    }
    
    // Товары комплета
    $products_kit = $this->products->get_products_kit(array('main_product_id'=>$product->id));
    
    if(!empty($products_kit))
    {
        foreach($products_kit as &$p_k)
            $p_kit[$p_k->product_id] = &$p_k;

        $temp_variants = $this->variants->get_variants(array('product_id'=>array_keys($p_kit)));
        foreach($temp_variants as $temp_variant)
            $p_kit[$temp_variant->product_id]->variants[] = $temp_variant;
    }
    ...
    $this->design->assign('related_products', $related_products);
    $this->design->assign('products_kit', $products_kit);
    ...

Поправим шаблон отображения /simpla/design/html/product.tpl

$(function() {
    ...
    $('#set_checkbox:checkbox').change(function() {
      if ( $(this).attr("checked") ) {
        $('#set_block').show();
      }
      else {
        $('#set_block').hide();
      }
    });

    // Добавление товара в комплект
    var new_set = $('#new_set').clone(true).on('keyup', price_calc);
    $('#new_set').remove().removeAttr('id');
 
    $("input#set_products").autocomplete({
        serviceUrl:'ajax/search_products.php',
        minChars:0,
        noCache: false, 
        onSelect:
        function(suggestion){
            $("input#set_products").val('').focus().blur();
            new_item = $(new_set).clone(true).appendTo('#kits').fadeIn('slow');
            new_item.find('input[name="kit[product_name][]"]').val(suggestion.data.name);
            new_item.find('input[name="kit[product_id][]"]').val(suggestion.data.id);

            // Сумма для комплекта
            $('li.variant_discount > input').val( function(i, oldval) {
                if(isNaN(parseFloat(oldval))) {
                    oldval = 0;
                }
                return parseFloat(oldval) + parseFloat(suggestion.data.price);
            });

            $.get( 'ajax/variants_products.php', { product_id: suggestion.data.id }).done(function( data ) {
                new_item.find('select[name="kit[variant_id][]"]').append(data);
            });
            
            new_item.find('input[name="kit[price][]"]').val(suggestion.data.price);
            new_item.find('input[name="kit[price_old][]"]').val(suggestion.data.price);
        },
        formatResult:
            function(suggestions, currentValue){
                var reEscape = new RegExp('(\\' + ['/', '.', '*', '+', '?', '|', '(', ')', '[', ']', '{', '}', '\\'].join('|\\') + ')', 'g');
                var pattern = '(' + currentValue.replace(reEscape, '\\$1') + ')';
                return (suggestions.data.image?"<img align=absmiddle src='"+suggestions.data.image+"'> ":'') + suggestions.value.replace(new RegExp(pattern, 'gi'), '<strong>$1<\/strong>');
            }
    });
    
    // Удаление товара из комплекта
    $('a.del_kit').click(function() {
        if($("#kits ul").size()>0)
        {
            $(this).closest("ul").fadeOut(200, function() { $(this).remove(); });
        }
    
        return false;
    });

    $('#kits > ul').on('keyup', price_calc);
...
});
...
function price_calc(event) {
	var variant_price = 0;
	var variant_discount = 0;
	var kit_price_old = 0;
	var kit_amount = 0;
	var kit_price = 0;
	
	if(event.srcElement.name === 'kit[price][]' || event.srcElement.name === 'kit[amount][]') {
		$(event.target).closest('div').find('ul').each(function() {
			kit_price_old = $(this).find('[name="kit[price_old][]"]').val();
			kit_amount = $(this).find('[name="kit[amount][]"]').val();
			kit_price = $(this).find('[name="kit[price][]"]').val();
			
			variant_price += kit_amount * kit_price;
			variant_discount += kit_amount * kit_price_old;
		});
	}
	
	// Сумма для комплекта
	$('li.variant_price > input').val(variant_price);
	$('li.variant_discount > input').val(variant_discount);
}
</script>
...
<div class="checkbox">
    <input name=featured value="1" type="checkbox" id="featured_checkbox" {if $product->featured}checked{/if}/> <label for="featured_checkbox">Рекомендуемый</label>
</div>
<div class="checkbox">
    <input name=kit value='1' type="checkbox" id="set_checkbox" {if $product->kit}checked{/if}/> <label for="set_checkbox">Комплект</label>
</div>
...
    <!-- Варианты товара -->
    ...
    <!-- Варианты товара (The End)-->
    <!-- Комплект товара -->
    <div id="set_block" {if !$product->kit}style='display:none;'{/if}>
        <ul id="header">
            <li class="set_move"></li>
            <li class="set_name">Название варианта</li>
            <li class="set_sku">Вариант</li>
            <li class="set_price">Цена, {$currency->sign}</li>
            <li class="set_amount">Кол-во</li>
        </ul>
        <div id="kits">
        
        {foreach from=$products_kit item=kit}
        <ul>
            <li class="set_move"><div class="move_zone"></div></li>
            <li class="set_name">
                <input name="kit[id][]" type="hidden" value="{$kit->id|escape}" />
                <input name="kit[product_name][]" type="" value="{$kit->product_name|escape}" />
                <input name="kit[product_id][]" type="hidden" value="{$kit->product_id|escape}" />
                <a class="del_kit" href=""><img src="design/images/cross-circle-frame.png" alt="" /></a>
            </li>
            <li class="set_sku">
                <select name="kit[variant_id][]">
                    {foreach from=$kit->variants item=kit_variant}
                        <option value="{$kit_variant->id}">{if $kit_variant->name}{$kit_variant->name}{else}{$kit_variant->id}{/if}</option>
                    {/foreach}
                </select>
            </li>
            <li class="set_price">
                <input name="kit[price][]" type="text" value="{$kit->price|escape}" />
                <input name="kit[price_old][]" type="hidden" value="{$kit->price_old|escape}" />
            </li>
            <li class="set_amount"><input name="kit[amount][]" type="text" value="{if $kit->amount|escape}{$kit->amount|escape}{else}0{/if}" />{$settings->units}</li>
        </ul>
        {/foreach}
        
        
        </div>
        <ul id=new_set style='display:none;'>
            <li class="variant_move"><div class="move_zone"></div></li>
            <li class="set_name">
                <input name="kit[product_name][]" type="" value="" />
                <input name="kit[product_id][]" type="hidden" value="" />
                <a class="del_kit" href=""><img src="design/images/cross-circle-frame.png" alt="" /></a>
            </li>
            <li class="set_sku">
                <select name="kit[variant_id][]">
                </select>
            </li>
            <li class="set_price">
                <input name="kit[price][]" type="text" value="" />
                <input name="kit[price_old][]" type="hidden" value="" />
            </li>
            <li class="set_amount"><input name="kit[amount][]" type="text" value="1" />{$settings->units}</li>
        </ul>

        <input class="button_green button_save" type="submit" name="" value="Сохранить" />
        <span class="add_kit" ><input type=text name=collection_sug id='set_products' class="input_autocomplete" placeholder='Выберите товар чтобы добавить его' /></span>
        
     </div>
    <!-- Комплект товара (The End)-->

Добавим загрузку варианта. Создаем файл /simpla/ajax/variants_products.php

<?php
        require_once('../../api/Simpla.php');
        $simpla = new Simpla();
        $limit = 100;

        $product_id = $simpla->request->get('product_id', 'integer');

        $simpla->db->query('SELECT * FROM __variants WHERE product_id = ?', $product_id);

        $variants = $simpla->db->results();

        $options = "";
        foreach($variants as $variant)
        {
                $options .= "<option value='".$variant->id."' data-price='".$variant->price."'>".($variant->name?$variant->name:$variant->id)."</option>";
        }

        header("Content-type: application/json; charset=UTF-8");
        header("Cache-Control: must-revalidate");
        header("Pragma: no-cache");
        header("Expires: -1");
        print json_encode($options);

Изменить файл /simpla/ajax/search_products.php

...
	$simpla->db->query('SELECT p.id, p.name, i.filename as image, v.price FROM __products p
                        LEFT JOIN __images i ON i.product_id=p.id AND i.position=(SELECT MIN(position) FROM __images WHERE product_id=p.id LIMIT 1)
                        LEFT JOIN __variants v ON v.product_id = p.id
	                    WHERE p.name LIKE "%'.mysql_real_escape_string($keyword).'%" ORDER BY p.name LIMIT ?', $limit);
	$products = $simpla->db->results();
...

Поправим стили /simpla/design/css/style.css

/* Комплект товара */
#set_block {
	background-color: #f0f0f0;
	border: 1px dotted #d0d0d0;
	clear: both;
	overflow: hidden;
	padding-top: 15px;
	padding-bottom: 15px;
	margin-bottom: 20px;
	width: 940px;
}
#set_block ul  {
	overflow: hidden;
	padding-bottom: 2px;
	padding-left:20px;
	padding-top: 2px;
	clear: left;
}
#set_block ul#header {
	height: 20px;
	margin-bottom: 0;
	padding-bottom: 0;
}
#set_block ul#header li {
	font-size: 13px;
	padding-left: 3px;
	margin-left: -2px;
}
#set_block ul li {
	float: left;
	display: block;
	height: 31px;
	overflow: hidden;
}
#set_block ul li input {
	height: 20px;
	font-size: 14px;
	color: #636363;
	margin-top: 0px;
	padding-top: 2px;
	margin-left: 2px;
}
#set_block li.set_move {	width: 20px; height:16px; padding-top: 5px;}
#set_block li.set_name { width: 270px;	padding-left: 5px; }
#set_block li.set_name input {
	font-size: 16px;
	color: #000;
	width: 220px;
	float: left;
	margin-right: 2px;
}
#set_block li.set_name a.del_variant {
	display: block;
	padding-top: 6px;
}
#set_block li.set_sku { width: 120px; }
#set_block li.set_sku input { width: 95px; }
#set_block li.set_price { width: 120px; }
#set_block li.set_price input {
	width: 95px;
	font-size: 15px;
	font-weight: bold;
	color: #000;
}
#set_block li.set_discount { width: 95px; }
#set_block li.set_discount input { width: 70px; }
#set_block li.set_amount { width: 90px; font-size: 11px; }
#set_block li.set_amount input { width: 35px; margin-right: 2px; }
#set_block li.set_download { width: 180px;}
#set_block li.set_download input{ width: 170px; font-size: 9px; margin-top: 2px;}
#set_block li.set_download .add_attachment img{  margin-top: 4px;}


#set_block span.add {
	margin-left: 30px;
	clear: left;
	margin-top: 5px;
	display: block;
	background-repeat: no-repeat;
	padding-left: 20px;	
	background-image: url(../images/plus-circle.png);
}
#set_block .variant_move div{
	width:20px;
	height: 16px;
}

#set_block span.add_kit {
	margin-left: 8px;
	clear: left;
	margin-top: 5px;
	display: block;
	background-repeat: no-repeat;
	padding-left: 40px;	
}

Отображаем комплект

Отображение карточки комплекта /view/ProductView.php

if(!empty($related_ids))
    {
        ...
    }
    
    // Товары коллекции
    $products_kit = $this->products->get_products_kit(array('main_product_id'=>$product->id));
    if(!empty($products_kit))
    {
        foreach($products_kit as &$p_k)
            $p_kit[$p_k->product_id] = &$p_k;

        $temp_products = $this->products->get_products(array('id'=>array_keys($p_kit)));
        foreach($temp_products as $temp_product)
        {
            $p_kit[$temp_product->id]->url = $temp_product->url;
            $p_kit[$temp_product->id]->name = $temp_product->name;
        }
        
        $temp_images = $this->products->get_images(array('product_id'=>array_keys($p_kit)));
        foreach($temp_images as $temp_image)
        {
            $p_kit[$temp_image->product_id]->image = $temp_image->filename;
        }
    }

заменить

return $this->design->fetch('product.tpl');

на

if($product->kit)
    {
        $this->design->assign('products_kit', $products_kit);
        return $this->design->fetch('products_kit.tpl');
    }
    else
    {
        return $this->design->fetch('product.tpl');
    }

Шаблон отображения /public_html/design/spy-tv/html/products_kit.tpl

{foreach name=products_kit item=product_kit from=$products_kit}
    <div class="product">
        
        <!-- Фото товара -->
        <div class="image">
            {if $product_kit->image}
            <a href="/products/{$product_kit->url}"><img src="{$product_kit->image|resize:35:35}" alt="{$product_kit->name|escape}"/></a>
            {else}
            <a href="/products/{$product_kit->url}"><img src="design/{$settings->theme|escape}/images/nofoto.gif"/></a>
            {/if}
        </div>
        <!-- Фото товара (The End) -->

        <!-- Название товара -->
        <h4><a data-product="{$product_kit->id}" href="products/{$product_kit->url}">{$product_kit->name|escape}</a></h4>
        <!-- Название товара (The End) -->

        <!-- Цена товара -->
        <table>
        <tr class="variant">
            <td>
                {if $product_kit->price_old > 0}<span class="compare_price">{$product_kit->price_old|convert}</span>{/if}
                <span class="price">{$product_kit->price|convert} <span class="currency">{$currency->sign|escape}</span></span>
            </td>
        </tr>
        </table>
        <!-- Цена товара (The End) -->
    </div>
    {/foreach}

Заказ комплекта

Далее код описан для измененной работы корзины, код требует доработки для работы с simpla из коробки /public_html/view/CartView.php

// Старая реализация Добавляем товары к заказу
    /*foreach($this->request->post('amounts') as $variant_id=>$amount)
    {
        $this->orders->add_purchase(array('order_id'=>$order_id, 'variant_id'=>intval($variant_id), 'amount'=>intval($amount)));
    }*/

    // Добавляем товары к заказу
    foreach($this->request->post('amounts') as $variant_id=>$amount)
    {
        if($product->kit) //Если среди товаров есть комплект, то в покупку мы кладем товары комплекта
        {
            $products_kit = $this->products->get_products_kit(array('main_product_id'=>$product->id));
            foreach($products_kit as $p_k)
            {
                $this->orders->add_purchase(array('order_id'=>$order_id, 'variant_id'=>intval($p_k->variant_id), 'amount'=>($amount * intval($p_k->amount)), 'price'=>intval($p_k->price)));
            }
        }
        else
        {
            $this->orders->add_purchase(array('order_id'=>$order_id, 'variant_id'=>intval($variant_id), 'amount'=>intval($amount)));
        }
    }
2015   simpla

nginx для Эгея в папке example.com/blog/

Решил я вести блог в отдельной папке /blog/. В сторону apache не смотрю, потому отправной точкой послужил конфиг с сайта движка. Чтобы сайт корректно работал в папке, требуется заменить его часть на:

location /blog/ {
	root   /var/www/html/example;
	index  index.php index.html index.htm home.php;
	log_not_found  off;
	rewrite ^/blog/(.*)$ /blog/index.php?go=$1 last;
}
2015   e2   nginx