Skip to main content

add-tag

/**
 * GetCourse Offer Tag Manager
 * Скрипт для управления обязательными тегами офферов
 *
 * Формат тега: TYPE:PRODUCT:CAT (например: SPR:MK:P)
 */

(function() {
    'use strict';

    // ========================================
    // КОНФИГУРАЦИЯ
    // ========================================
    const Config = {
        // Google Sheets
        spreadsheetId: '1JM5CUl8xm9sB8CyvyVZCXZyxbjjO2z2jEP7Sp1h7Zqs',
        sheets: ['TYPE', 'PRODUCT', 'CAT'],

        // Формат обязательного тега
        tagFormat: {
            separator: ':',
            // Базовый паттерн: 3 части через двоеточие, заглавные буквы/цифры
            pattern: /^[A-Z0-9]+:[A-Z0-9]+:[A-Z]$/,
            // Строгий паттерн с известными значениями (будет обновлен после загрузки данных)
            strictPattern: null
        },

        // Селекторы GetCourse
        selectors: {
            offerForm: '#offerForm',
            headerWithTags: '.header-with-tags',
            offerUpdate: '.offer-update',
            saveButton: '.save-offer',
            copyButton: '.copy-offer',
            // Селекторы для чтения тегов
            tagsContainer: '.gc-tags-editable',
            tagsInputName: 'Offer[tags][]'
        },

        // UI настройки
        ui: {
            containerId: 'mandatory-tag-container',
            selectClass: 'mandatory-tag-select'
        },

        // Сообщения
        messages: {
            selectPlaceholder: '-- Выберите --',
            missingParams: 'Выберите все параметры обязательного тега',
            validTag: 'Тег сформирован',
            copyHint: 'Скопируйте и вставьте тег в список тегов оффера',
            missingTag: 'Обязательный тег отсутствует в списке тегов оффера',
            invalidTag: 'Обязательный тег имеет неверный формат',
            multipleTags: 'Найдено несколько обязательных тегов. Должен быть ровно один.',
            tagFound: 'Обязательный тег найден в списке',
            labels: {
                type: 'Тип',
                product: 'Продукт',
                cat: 'Категория',
                preview: 'Тег',
                title: 'Обязательный тег оффера'
            }
        }
    };

    // ========================================
    // ЗАГРУЗЧИК ДАННЫХ
    // ========================================
    const DataLoader = {
        cache: {},

        /**
         * URL для Google Visualization Query API
         */
        buildUrl(sheetName) {
            return `https://docs.google.com/spreadsheets/d/${Config.spreadsheetId}/gviz/tq?tqx=out:json&sheet=${encodeURIComponent(sheetName)}`;
        },

        /**
         * Парсинг ответа Google Visualization API
         */
        parseGvizResponse(responseText) {
            try {
                // Удаляем JSONP обертку: google.visualization.Query.setResponse({...});
                const jsonStr = responseText
                    .replace(/^[^(]*\(/, '')
                    .replace(/\);?\s*$/, '');

                const data = JSON.parse(jsonStr);
                const rows = data.table.rows || [];

                // Пропускаем первую строку (заголовки)
                const dataRows = rows.slice(1);

                return dataRows.map(row => {
                    const cells = row.c || [];
                    return {
                        code: cells[0]?.v?.toString() || '',
                        label: cells[1]?.v?.toString() || cells[0]?.v?.toString() || '',
                        description: cells[2]?.v?.toString() || ''
                    };
                }).filter(item => item.code && item.code.trim());
            } catch (e) {
                console.error('[OfferTagManager] Parse error:', e);
                return [];
            }
        },

        /**
         * Загрузить один лист
         */
        async loadSheet(sheetName) {
            if (this.cache[sheetName]) {
                return this.cache[sheetName];
            }

            const url = this.buildUrl(sheetName);

            const response = await fetch(url);
            if (!response.ok) {
                throw new Error(`HTTP ${response.status} for sheet ${sheetName}`);
            }

            const text = await response.text();
            const data = this.parseGvizResponse(text);

            if (data.length === 0) {
                throw new Error(`Empty data for sheet ${sheetName}`);
            }

            this.cache[sheetName] = data;
            return data;
        },

        /**
         * Загрузить все листы параллельно
         */
        async loadAllSheets() {
            const results = await Promise.all(
                Config.sheets.map(sheet =>
                    this.loadSheet(sheet)
                        .then(data => ({ sheet, data, error: null }))
                        .catch(error => ({ sheet, data: [], error }))
                )
            );

            const sheetsData = {};
            const errors = [];

            for (const result of results) {
                if (result.error) {
                    errors.push(`${result.sheet}: ${result.error.message}`);
                } else {
                    sheetsData[result.sheet] = result.data;
                }
            }

            if (errors.length > 0) {
                throw new Error(errors.join('; '));
            }

            return sheetsData;
        }
    };

    // ========================================
    // ПАРСЕР ТЕГОВ
    // ========================================
    const TagParser = {
        knownValues: {
            TYPE: [],
            PRODUCT: [],
            CAT: []
        },

        /**
         * Установить известные значения из загруженных данных
         */
        setKnownValues(sheetsData) {
            this.knownValues.TYPE = sheetsData.TYPE.map(item => item.code);
            this.knownValues.PRODUCT = sheetsData.PRODUCT.map(item => item.code);
            this.knownValues.CAT = sheetsData.CAT.map(item => item.code);

            // Построить строгий regex
            const typePattern = this.knownValues.TYPE.join('|');
            const productPattern = this.knownValues.PRODUCT.join('|');
            const catPattern = this.knownValues.CAT.join('|');

            Config.tagFormat.strictPattern = new RegExp(
                `^(${typePattern}):(${productPattern}):(${catPattern})$`
            );
        },

        /**
         * Проверить, похож ли тег на обязательный (по формату)
         */
        isMandatoryTagFormat(tag) {
            return Config.tagFormat.pattern.test(tag);
        },

        /**
         * Строгая проверка тега (известные значения)
         */
        isValidMandatoryTag(tag) {
            if (!Config.tagFormat.strictPattern) {
                return this.isMandatoryTagFormat(tag);
            }
            return Config.tagFormat.strictPattern.test(tag);
        },

        /**
         * Разобрать тег на компоненты
         */
        parseTag(tag) {
            if (!this.isMandatoryTagFormat(tag)) {
                return null;
            }

            const parts = tag.split(Config.tagFormat.separator);
            return {
                type: parts[0],
                product: parts[1],
                cat: parts[2]
            };
        },

        /**
         * Собрать тег из компонентов
         */
        buildTag(type, product, cat) {
            if (!type || !product || !cat) return null;
            return [type, product, cat].join(Config.tagFormat.separator);
        },

        /**
         * Найти обязательные теги в массиве
         */
        findMandatoryTags(tags) {
            const mandatory = tags.filter(tag => {
                const matches = this.isMandatoryTagFormat(tag);
                console.log(`[OfferTagManager] Tag "${tag}" matches pattern: ${matches}`);
                return matches;
            });
            return {
                tags: mandatory,
                count: mandatory.length,
                first: mandatory[0] || null
            };
        }
    };

    // ========================================
    // ЧТЕНИЕ ТЕГОВ ИЗ GC-TAGS-EDITABLE
    // ========================================
    const TagReader = {
        $container: null,

        /**
         * Инициализация
         */
        init() {
            this.$container = $(Config.selectors.tagsContainer).first();
            return this.$container.length > 0;
        },

        /**
         * Получить текущие теги
         */
        getCurrentTags() {
            const tags = [];

            // Способ 1: из hidden inputs (самый надёжный)
            $(Config.selectors.tagsContainer)
                .find(`input[name="${Config.selectors.tagsInputName}"]`)
                .each(function() {
                    const val = $(this).val();
                    if (val && val.trim()) {
                        tags.push(val.trim());
                    }
                });

            if (tags.length > 0) {
                console.log('[OfferTagManager] Found tags from inputs:', tags);
                return tags;
            }

            // Способ 2: из data-атрибута
            const dataTags = $(Config.selectors.tagsContainer).attr('data-tags');
            if (dataTags) {
                const parsed = dataTags.split(',').map(t => t.trim()).filter(Boolean);
                console.log('[OfferTagManager] Found tags from data-tags:', parsed);
                return parsed;
            }

            console.log('[OfferTagManager] No tags found');
            return [];
        },

        /**
         * Найти обязательные теги в списке
         */
        findMandatoryTags() {
            const allTags = this.getCurrentTags();
            const result = TagParser.findMandatoryTags(allTags);
            console.log('[OfferTagManager] Mandatory tags check:', result);
            return result;
        }
    };

    // ========================================
    // ВАЛИДАЦИЯ
    // ========================================
    const Validation = {
        $saveButton: null,
        $copyButton: null,

        /**
         * Инициализация
         */
        init() {
            this.$saveButton = $(Config.selectors.saveButton);
            this.$copyButton = $(Config.selectors.copyButton);

            this._interceptFormSubmit();
            this._setupTagsObserver();
        },

        /**
         * Перехват отправки формы
         */
        _interceptFormSubmit() {
            const $form = $(Config.selectors.offerForm);
            const self = this;

            $form.on('submit', function(e) {
                const result = self.validate();

                if (!result.valid) {
                    e.preventDefault();
                    e.stopImmediatePropagation();

                    UIBuilder.updateStatus(result.error, 'danger');

                    // Прокрутка к блоку
                    const $container = $(`#${Config.ui.containerId}`);
                    if ($container.length) {
                        $('html, body').animate({
                            scrollTop: $container.offset().top - 100
                        }, 300);
                    }

                    alert(result.error);
                    return false;
                }
            });
        },

        /**
         * Наблюдатель за изменениями в контейнере тегов
         */
        _setupTagsObserver() {
            const $tagsContainer = $(Config.selectors.tagsContainer);
            if (!$tagsContainer.length) return;

            const self = this;
            const observer = new MutationObserver(() => {
                console.log('[OfferTagManager] Tags changed, re-validating...');
                self.updateButtonsState();
            });

            observer.observe($tagsContainer[0], {
                attributes: true,
                childList: true,
                subtree: true,
                characterData: true
            });
        },

        /**
         * Валидация
         */
        validate() {
            const mandatory = TagReader.findMandatoryTags();

            // Проверка 1: тег должен быть
            if (mandatory.count === 0) {
                return { valid: false, error: Config.messages.missingTag };
            }

            // Проверка 2: ровно один тег
            if (mandatory.count > 1) {
                return { valid: false, error: Config.messages.multipleTags };
            }

            // Проверка 3: тег валиден (из известных значений)
            if (!TagParser.isValidMandatoryTag(mandatory.first)) {
                return { valid: false, error: Config.messages.invalidTag };
            }

            return { valid: true, error: null, tag: mandatory.first };
        },

        /**
         * Обновить состояние кнопок
         */
        updateButtonsState() {
            const result = this.validate();

            if (result.valid) {
                this.$saveButton.prop('disabled', false);
                this.$copyButton.prop('disabled', false);
                UIBuilder.updateStatus(Config.messages.tagFound + ': ' + result.tag, 'success');
            } else {
                this.$saveButton.prop('disabled', true);
                this.$copyButton.prop('disabled', true);
                UIBuilder.updateStatus(result.error, 'danger');
            }
        }
    };

    // ========================================
    // ПОСТРОЕНИЕ UI
    // ========================================
    const UIBuilder = {
        /**
         * Создать основной контейнер
         */
        createTagSelector(sheetsData) {
            const $container = $('<div>')
                .attr('id', Config.ui.containerId)
                .addClass('panel panel-info')
                .css({
                    marginTop: '15px',
                    marginBottom: '15px'
                });

            // Заголовок панели
            const $heading = $('<div>')
                .addClass('panel-heading')
                .html(`<strong>${Config.messages.labels.title}</strong>`);

            // Тело панели
            const $body = $('<div>')
                .addClass('panel-body')
                .css('paddingBottom', '10px');

            // Строка с селектами
            const $row = $('<div>').addClass('row');

            // TYPE селект
            $row.append(this._createSelectColumn('type', Config.messages.labels.type, sheetsData.TYPE));

            // PRODUCT селект
            $row.append(this._createSelectColumn('product', Config.messages.labels.product, sheetsData.PRODUCT));

            // CAT селект
            $row.append(this._createSelectColumn('cat', Config.messages.labels.cat, sheetsData.CAT));

            // Превью тега с кнопкой копирования
            const $preview = $('<div>')
                .addClass('col-md-3 col-sm-6')
                .html(`
                    <div class="form-group" style="margin-bottom: 0">
                        <label>${Config.messages.labels.preview}</label>
                        <div style="display: flex; align-items: center; gap: 8px;">
                            <div id="mandatory-tag-preview"
                                 style="font-family: monospace; font-weight: bold; font-size: 15px;
                                        padding: 7px 12px; min-height: 34px; color: #999;
                                        border: 1px dashed #ccc; border-radius: 4px; background: #f9f9f9;">
                                —
                            </div>
                            <button type="button" id="copy-mandatory-tag"
                                    style="display: none; border: none; background: none; cursor: pointer;
                                           padding: 6px 10px; border-radius: 4px; transition: all 0.2s ease;"
                                    title="Скопировать тег">
                                <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="#5cb85c" stroke-width="2">
                                    <rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
                                    <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
                                </svg>
                            </button>
                        </div>
                        <div id="copy-feedback" style="display: none; color: #5cb85c; font-size: 11px; margin-top: 4px;">
                            Скопировано
                        </div>
                    </div>
                `);
            $row.append($preview);

            $body.append($row);

            // Статус
            const $status = $('<div>')
                .attr('id', 'mandatory-tag-status')
                .addClass('alert alert-warning')
                .css({ marginTop: '10px', marginBottom: '0' })
                .text(Config.messages.missingParams);
            $body.append($status);

            $container.append($heading).append($body);

            return $container;
        },

        /**
         * Создать колонку с селектом
         */
        _createSelectColumn(name, label, options) {
            const selectId = `mandatory-tag-${name}`;

            const $col = $('<div>').addClass('col-md-3 col-sm-6');

            const $group = $('<div>')
                .addClass('form-group')
                .css('marginBottom', '0');

            $group.append(
                $('<label>')
                    .attr('for', selectId)
                    .text(label)
            );

            const $select = $('<select>')
                .attr('id', selectId)
                .addClass(`form-control ${Config.ui.selectClass}`)
                .attr('data-param', name)
                .attr('data-placeholder', Config.messages.selectPlaceholder)
                .css('fontFamily', 'monospace');

            // Пустая опция (для Select2 placeholder)
            $select.append(
                $('<option>')
                    .val('')
                    .text('')
            );

            // Опции из данных
            for (const item of options) {
                $select.append(
                    $('<option>')
                        .val(item.code)
                        .text(`${item.code} — ${item.label}`)
                        .attr('title', item.description || '')
                );
            }

            $group.append($select);
            $col.append($group);

            return $col;
        },

        /**
         * Обновить превью тега
         */
        updatePreview(tag) {
            const $preview = $('#mandatory-tag-preview');
            const $copyBtn = $('#copy-mandatory-tag');

            if (tag) {
                $preview.text(tag).css('color', '#3c763d');
                $copyBtn.show();
            } else {
                $preview.text('—').css('color', '#999');
                $copyBtn.hide();
            }

            // Скрыть feedback при изменении
            $('#copy-feedback').hide();
        },

        /**
         * Копировать тег в буфер обмена
         */
        copyTagToClipboard() {
            const tag = $('#mandatory-tag-preview').text();
            if (!tag || tag === '—') return;

            navigator.clipboard.writeText(tag).then(() => {
                // Показать feedback
                const $feedback = $('#copy-feedback');
                $feedback.show();

                // Скрыть через 2 секунды
                setTimeout(() => {
                    $feedback.fadeOut();
                }, 2000);

                console.log('[OfferTagManager] Tag copied:', tag);
            }).catch(err => {
                // Fallback для старых браузеров
                const textArea = document.createElement('textarea');
                textArea.value = tag;
                textArea.style.position = 'fixed';
                textArea.style.left = '-999999px';
                document.body.appendChild(textArea);
                textArea.select();

                try {
                    document.execCommand('copy');
                    $('#copy-feedback').show();
                    setTimeout(() => {
                        $('#copy-feedback').fadeOut();
                    }, 2000);
                } catch (e) {
                    alert('Не удалось скопировать. Выделите тег вручную и нажмите Ctrl+C');
                }

                document.body.removeChild(textArea);
            });
        },

        /**
         * Обновить статус
         */
        updateStatus(message, type) {
            const $status = $('#mandatory-tag-status');
            $status
                .removeClass('alert-success alert-warning alert-danger alert-info')
                .addClass(`alert-${type}`)
                .text(message);
        },

        /**
         * Заполнить селекты из тега
         */
        fillSelectsFromTag(parsed) {
            if (!parsed) return;

            $('#mandatory-tag-type').val(parsed.type);
            $('#mandatory-tag-product').val(parsed.product);
            $('#mandatory-tag-cat').val(parsed.cat);
        },

        /**
         * Получить значения из селектов
         */
        getSelectValues() {
            return {
                type: $('#mandatory-tag-type').val(),
                product: $('#mandatory-tag-product').val(),
                cat: $('#mandatory-tag-cat').val()
            };
        }
    };

    // ========================================
    // ГЛАВНЫЙ МОДУЛЬ
    // ========================================
    const OfferTagManager = {
        initialized: false,
        sheetsData: null,
        currentMandatoryTag: null,

        /**
         * Проверка страницы
         */
        isOfferEditPage() {
            return /\/pl\/sales\/offer\/update\?id=\d+/.test(window.location.href);
        },

        /**
         * Точка входа
         */
        async init() {
            if (!this.isOfferEditPage()) {
                return;
            }

            console.log('[OfferTagManager] Initializing...');

            try {
                await this._waitForDependencies();

                // Загрузка данных
                this.sheetsData = await DataLoader.loadAllSheets();
                console.log('[OfferTagManager] Data loaded:', this.sheetsData);

                // Установить известные значения
                TagParser.setKnownValues(this.sheetsData);

                // Построить UI
                this._buildUI();

                // События
                this._bindEvents();

                // Валидация
                Validation.init();
                Validation.updateButtonsState();

                // Стили
                this._injectStyles();

                this.initialized = true;
                console.log('[OfferTagManager] Initialized successfully');

            } catch (error) {
                console.error('[OfferTagManager] Initialization failed:', error);
                this._showError(error.message);
            }
        },

        /**
         * Ожидание зависимостей
         */
        _waitForDependencies() {
            return new Promise((resolve, reject) => {
                let attempts = 0;
                const maxAttempts = 100; // 10 секунд

                const check = () => {
                    attempts++;

                    if (typeof jQuery !== 'undefined' &&
                        jQuery(Config.selectors.offerForm).length > 0) {
                        resolve();
                        return;
                    }

                    if (attempts >= maxAttempts) {
                        reject(new Error('Timeout waiting for page elements'));
                        return;
                    }

                    setTimeout(check, 100);
                };

                check();
            });
        },

        /**
         * Построение UI
         */
        _buildUI() {
            const $selector = UIBuilder.createTagSelector(this.sheetsData);

            // Вставка: после .header-with-tags или перед формой
            const $header = $(Config.selectors.headerWithTags);
            const $offerUpdate = $(Config.selectors.offerUpdate);

            if ($header.length) {
                $header.after($selector);
            } else if ($offerUpdate.length) {
                $offerUpdate.find('h1').first().after($selector);
            } else {
                $(Config.selectors.offerForm).before($selector);
            }
        },

        /**
         * Привязка событий
         */
        _bindEvents() {
            const self = this;

            // Инициализация Select2 для поиска в выпадающих списках
            if (typeof $.fn.select2 !== 'undefined') {
                $(`.${Config.ui.selectClass}`).select2({
                    placeholder: Config.messages.selectPlaceholder,
                    allowClear: true,
                    width: '100%',
                    language: {
                        noResults: function() { return "Ничего не найдено"; },
                        searching: function() { return "Поиск..."; }
                    }
                });
                console.log('[OfferTagManager] Select2 initialized');
            } else {
                console.warn('[OfferTagManager] Select2 not available, using standard selects');
            }

            // Изменение селектов (работает и с Select2, и без)
            $(`.${Config.ui.selectClass}`).on('change', function() {
                self._onSelectChange();
            });

            // Кнопка копирования тега
            $('#copy-mandatory-tag').on('click', function() {
                UIBuilder.copyTagToClipboard();
            });
        },

        /**
         * Обработчик изменения селекта
         */
        _onSelectChange() {
            const values = UIBuilder.getSelectValues();

            if (values.type && values.product && values.cat) {
                const newTag = TagParser.buildTag(values.type, values.product, values.cat);
                this.currentMandatoryTag = newTag;
                UIBuilder.updatePreview(newTag);
                UIBuilder.updateStatus(Config.messages.validTag + '. ' + Config.messages.copyHint, 'success');
            } else {
                this.currentMandatoryTag = null;
                UIBuilder.updatePreview(null);
                UIBuilder.updateStatus(Config.messages.missingParams, 'warning');
            }
        },

        /**
         * Показать ошибку
         */
        _showError(message) {
            const $error = $('<div>')
                .addClass('alert alert-danger')
                .css({ margin: '15px 0' })
                .html(`
                    <strong>Ошибка загрузки:</strong> ${message}<br>
                    <small>Попробуйте обновить страницу. Если проблема сохраняется, обратитесь в поддержку.</small>
                `);

            const $header = $(Config.selectors.headerWithTags);
            if ($header.length) {
                $header.after($error);
            } else {
                $(Config.selectors.offerForm).before($error);
            }

            // Заблокировать сохранение при ошибке загрузки
            $(Config.selectors.saveButton).prop('disabled', true);
            $(Config.selectors.copyButton).prop('disabled', true);
        },

        /**
         * Внедрение стилей
         */
        _injectStyles() {
            if ($('#offer-tag-manager-styles').length) return;

            const styles = `
                <style id="offer-tag-manager-styles">
                    #${Config.ui.containerId} {
                        border-color: #5bc0de;
                    }
                    #${Config.ui.containerId} .panel-heading {
                        background-color: #d9edf7;
                        border-color: #5bc0de;
                        color: #31708f;
                    }
                    #${Config.ui.containerId} .form-group label {
                        font-weight: 600;
                        color: #555;
                    }
                    #${Config.ui.containerId} select {
                        font-size: 13px;
                    }
                    #mandatory-tag-status {
                        transition: all 0.2s ease;
                    }
                    #copy-mandatory-tag:hover {
                        background: rgba(92, 184, 92, 0.1) !important;
                    }
                    #copy-mandatory-tag:hover svg {
                        stroke: #449d44;
                    }
                    #copy-mandatory-tag:active {
                        transform: scale(0.95);
                    }
                    /* Select2 стили для панели */
                    #${Config.ui.containerId} .select2-container {
                        width: 100% !important;
                    }
                    #${Config.ui.containerId} .select2-container--default .select2-selection--single {
                        height: 34px;
                        border: 1px solid #ccc;
                        border-radius: 4px;
                    }
                    #${Config.ui.containerId} .select2-container--default .select2-selection--single .select2-selection__rendered {
                        line-height: 32px;
                        font-family: monospace;
                        font-size: 13px;
                    }
                    #${Config.ui.containerId} .select2-container--default .select2-selection--single .select2-selection__arrow {
                        height: 32px;
                    }
                    #${Config.ui.containerId} .select2-container--default .select2-selection--single .select2-selection__placeholder {
                        color: #999;
                    }
                    .select2-container--default .select2-results__option {
                        font-family: monospace;
                        font-size: 13px;
                    }
                    .select2-container--default .select2-results__option--highlighted[aria-selected] {
                        background-color: #5bc0de;
                    }
                    .select2-container--default .select2-search--dropdown .select2-search__field {
                        font-size: 13px;
                    }
                </style>
            `;

            $('head').append(styles);
        }
    };

    // ========================================
    // АВТОЗАПУСК
    // ========================================
    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', function() {
            setTimeout(function() { OfferTagManager.init(); }, 100);
        });
    } else {
        setTimeout(function() { OfferTagManager.init(); }, 100);
    }

})();