Загрузка
Все статьи

Как я релизовал динамический виджет партнёров для туров на WordPress + Bitrix24

При разработке одного из проектов на WordPress с использованием конструктора туров TourMaster передо мной встала задача: связать каждый день тура с конкретным партнёром, а затем синхронизировать эти данные с CRM системой Bitrix24. В этой статье я расскажу, как мне удалось решить эту задачу, не модифицируя исходный код плагина.

Допустим, у нас есть:

Архитектура решения

Основная идея: не трогать код плагина, а работать через JavaScript, добавляя свои виджеты в уже существующие DOM-элементы.

Вот как выглядит итоговый виджет для каждого дня:

function buildDateWidget(dateIndex, allPartners, defaultPartner) {
    var storageKey = 'pa_date_' + dateIndex;
    
    var widget = document.createElement('div');
    widget.className = 'pa-date-widget';
    widget.setAttribute('data-storage-key', storageKey);
    
    var select = document.createElement('select');
    select.className = 'pa-partner-select';
    
    // Добавляем опции для всех партнёров
    allPartners.forEach(function (p) {
        var opt = document.createElement('option');
        opt.value = p.id;
        opt.textContent = p.name;
        select.appendChild(opt);
    });
    
    var overrideInput = document.createElement('input');
    overrideInput.type = 'text';
    overrideInput.className = 'pa-override-input';
    overrideInput.placeholder = 'Доп. поле';
    
    widget.appendChild(select);
    widget.appendChild(overrideInput);
    
    return widget;
}

Ключевые моменты реализации

1. Получение списка партнёров из таксономии

Партнёры хранятся в WordPress-таксономии, которая рендерится через чекбоксы с определённой структурой. Мне нужно было извлечь всех партнёров и определить, кто из них выбран по умолчанию:

function getAllPartners() {
    var partners = [];
    $('#partnersB24 input[name="tour_partners_json[]"]').each(function () {
        var $cb = $(this);
        var $label = $cb.closest('label.selectit');
        partners.push({
            id: $cb.val(),
            name: $label.length ? $label.text().trim() : $cb.val(),
            checked: $cb.is(':checked')
        });
    });
    return partners;
}

function getDefaultPartner() {
    var $cb = $('#partnersB24 input[name="tour_partners_json[]"]:checked').first();
    if (!$cb.length) return null;
    var $label = $cb.closest('label.selectit');
    return {
        id: $cb.val(),
        name: $label.length ? $label.text().trim() : $cb.val()
    };
}

2. Нахождение всех секций с датами

TourMaster генерирует для каждого дня отдельную вкладку с определённым классом. Моя задача — найти все такие секции и внедрить в каждую свой виджет:

function findAllKvorumFields() {
    return document.querySelectorAll('.tourmaster-html-option-tabs-field[data-wrapper-slug="kworum"]');
}

3. Локальное сохранение данных

Чтобы при случайном обновлении страницы данные не терялись, я использую localStorage. Каждый виджет получает уникальный ключ:

function saveDateData(key, data) {
    try {
        data.ts = Date.now();
        localStorage.setItem(key, JSON.stringify(data));
    } catch (e) {
        console.warn('[PA] localStorage error:', e);
    }
}

4. Интеграция с Select2

Для улучшения пользовательского опыта я использовал библиотеку Select2, которая добавляет поиск и красивое оформление выпадающих списков:

if ($ && $.fn.select2) {
    $(select).select2({
        placeholder: 'Выберите партнёра',
        allowClear: true,
        width: '100%',
        language: {
            noResults: function () { return 'Партнёр не найден'; },
            searching: function () { return 'Поиск...'; }
        }
    });
}

5. Синхронизация с Bitrix24

При изменении значения в любом виджете я обновляю скрытое поле, которое отправляется в CRM:

function syncWithBitrix(value) {
    var field = document.querySelector('input[name="' + FIELD_KEY + '"]');
    if (!field) {
        field = document.createElement('input');
        field.type = 'hidden';
        field.name = FIELD_KEY;
        var form = document.querySelector('form');
        if (form) {
            form.appendChild(field);
        }
    }
    field.value = value || '';
    field.dispatchEvent(new Event('change', { bubbles: true }));
}

6. Отправка данных при публикации

Перехватываем клик по кнопке «Опубликовать» и отправляем собранные данные через sendBeacon или fetch с keepalive:

function sendToB24In(data) {
    var payload = new URLSearchParams({
        action: 'sync_dates',
        tour_name: data.tour_name,
        partner_id: data.partner_id,
        partner_name: data.partner_name,
        dates: JSON.stringify(data.dates),
        modified_dates: JSON.stringify(modifiedDates)
    });
    
    var blob = new Blob([payload.toString()], {
        type: 'application/x-www-form-urlencoded'
    });
    navigator.sendBeacon('/assets/partners/b24in.php', blob);
}

7. Восстановление из JSON-файла

Отдельная история — возможность загрузить уже сохранённые данные из JSON-файла при редактировании тура:

function loadFromSyncJson() {
    fetch(SYNC_JSON_URL, { cache: 'no-store' })
        .then(function (res) { return res.json(); })
        .then(function (records) {
            // Ищем тур по названию
            var tourNorm = normalizeTourName(tourName);
            var matchedRecord = records.find(function(rec) {
                var recNorm = normalizeTourName(rec.tour_name || '');
                return recNorm === tourNorm || 
                       recNorm.indexOf(tourNorm) !== -1 ||
                       tourNorm.indexOf(recNorm) !== -1;
            });
            
            // Восстанавливаем данные для каждой даты
            // ...
        });
}

8. Обработка динамического добавления дат

TourMaster динамически добавляет и удаляет даты через JavaScript. Мне нужно было перехватить эти события и пересоздать виджеты:

$(document).on('click', '.tourmaster-html-option-tabs-add', function (e) {
    var $parent = $(this).closest('.tourmaster-html-option-item-title');
    
    // Проверяем, что это кнопка добавления даты, а не цены
    if ($parent.length && $parent.find('b.add-pr').length) {
        setTimeout(function () {
            refreshAllWidgets();
        }, 500);
    }
});

Стилизация

Чтобы виджет выглядел органично, я добавил современные CSS-стили с анимацией при появлении:

@keyframes paFadeIn {
    from { opacity: 0; transform: translateY(6px); }
    to { opacity: 1; transform: translateY(0); }
}

.pa-date-widget {
    padding: 12px 14px;
    border-radius: 10px;
    background: linear-gradient(135deg, rgba(255,255,255,0.92), rgba(240,255,244,0.88));
    backdrop-filter: blur(6px);
    border: 1px solid rgba(76,175,80,0.18);
    animation: paFadeIn 0.35s ease-out both;
}

Структура данных для отправки

Финальная структура данных, которая отправляется в b24in.php:

{
    tour_name: "Название тура",
    partner_id: "123",
    partner_name: "Имя партнёра по умолчанию",
    dates: [
        {
            date: "2024-01-15",
            partner_id: "456",
            partner_name: "Конкретный партнёр для дня",
            override_value: "Текст из дополнительного поля",
            final_partner: "Что будет показано в итоге",
            is_modified: true
        },
        // ...
    ]
}

В результате мне удалось:

  1. Не модифицировать исходный код плагина — все изменения внедряются через JavaScript

  2. Обеспечить сохранность данных — при перезагрузке страницы выбор партнёров восстанавливается

  3. Интегрироваться с Bitrix24 — данные автоматически уходят в CRM при публикации

  4. Поддерживать динамическое добавление/удаление дат — виджеты автоматически пересоздаются

  5. Восстанавливать ранее сохранённые данные — через JSON-файл синхронизации

Такой подход позволяет гибко расширять функциональность существующей системы без риска потерять изменения при обновлении плагина. Код получился самодостаточным — его достаточно скопировать в отдельный JS-файл и подключить на странице редактирования тура.

Поделиться статьёй

Комментарии

0
0 / 2000

Пока нет комментариев

Будьте первым, кто оставит комментарий к этой статье

Читайте также

Сложности с разделами каталога в Битрикс24 через REST API: игра с деревом

Обновляете товар через API, передаете SECTION_ID, но он не появляется в нужной папке. В этой статье я объясняю разницу между простым SECTION_ID и сложным деревом инфоблоков, как правильно выстроить иерархию и почему метод crm.product.update не умеет создавать папки.

5 июля 2026 1 мин

CMake: модульная архитектура для большого C++ проекта — без ad-hoc include путей

Как мы реорганизовали CMake-структуру проекта на 15+ модулей, избавились от хаотичных include_directories и перешли на современный CMake с target-based зависимостями.

20 июня 2026 10 мин

Kotlin + C++: реальный кейс — видеопроцессинг на сервере через JNI

Как мы построили сервер видеотранскодинга на Kotlin REST API + C++ FFmpeg через JNI. Прокачка JNI_OnLoad, передача фреймов через DirectByteBuffer, обработка ошибок и бенчмарки.

19 июня 2026 13 мин