Загрузка данных


(async function() {
    if (window._isTfsCollecting) {
        alert("Сбор уже запущен!");
        return;
    }
    window._isTfsCollecting = true;

    const resultsMap = new Map();
    window._tfsResultsMap = resultsMap;

    // Плавающий оверлей со статусом
    const overlay = document.createElement("div");
    overlay.style.cssText = "position:fixed;top:15px;right:15px;z-index:999999;background:#107c41;color:#fff;padding:14px 20px;border-radius:8px;box-shadow:0 6px 18px rgba(0,0,0,0.4);font-family:Segoe UI,sans-serif;font-size:13px;max-width:380px;line-height:1.4;";
    overlay.innerHTML = `
        <div style="font-weight:600;font-size:14px;margin-bottom:4px;">Сбор заявок TFS</div>
        <div id="tfs-status" style="min-height:36px;font-size:12px;opacity:0.95;">Инициализация...</div>
        <div style="margin-top:10px;display:flex;gap:8px;">
            <button id="tfs-stop-btn" style="flex:1;padding:6px 10px;background:#fff;color:#222;border:none;border-radius:4px;cursor:pointer;font-weight:600;font-size:12px;">Остановить и выгрузить</button>
        </div>
    `;
    document.body.appendChild(overlay);

    let stopRequested = false;
    document.getElementById("tfs-stop-btn").onclick = () => {
        stopRequested = true;
        updateStatus("Остановка... Формирую CSV файл.");
    };

    function updateStatus(msg) {
        const el = document.getElementById("tfs-status");
        if (el) el.innerHTML = msg;
    }

    function downloadCSV(data) {
        let csvContent = "Номер заявки;Последний комментарий\n";
        for (const row of data) {
            const num = String(row.number || "").replace(/[\n\r;]/g, " ").trim();
            const comment = String(row.comment || "")
                .replace(/"/g, '""')
                .replace(/[\n\r]+/g, " ");
            csvContent += `"${num}";"${comment}"\n`;
        }
        const blob = new Blob(["\uFEFF" + csvContent], { type: 'text/csv;charset=utf-8;' });
        const url = URL.createObjectURL(blob);
        const a = document.createElement("a");
        a.href = url;
        a.download = `заявки_TFS_${new Date().toISOString().slice(0, 10)}.csv`;
        document.body.appendChild(a);
        a.click();
        document.body.removeChild(a);
        URL.revokeObjectURL(url);
    }

    // Извлечение ID и выбор приоритетного элемента для клика
    function findRowInfo(row) {
        const cells = Array.from(row.querySelectorAll('.grid-cell'));
        let id = null;

        for (const cell of cells) {
            const txt = cell.innerText.trim();
            if (/^\d{4,8}$/.test(txt)) {
                id = txt;
                break;
            }
        }

        if (!id) return null;

        const idCell = cells.find(c => c.innerText.trim() === id);
        const titleLink = row.querySelector('.work-item-title-link');
        const clickable = idCell || row || titleLink;

        return { id, clickable };
    }

    // Безопасный клик: отменяем переход по ссылке, сохраняя события
    function safeClick(element) {
        const targetAnchor = element.tagName === 'A' ? element : element.closest('a');
        if (targetAnchor) {
            targetAnchor.addEventListener('click', e => e.preventDefault(), { capture: true, once: true });
        }

        element.dispatchEvent(new MouseEvent('mousedown', { bubbles: true, cancelable: true }));
        element.dispatchEvent(new MouseEvent('mouseup',   { bubbles: true, cancelable: true }));
        element.dispatchEvent(new MouseEvent('click',     { bubbles: true, cancelable: true }));
    }

    // Определение ID открытой карточки формы в правой панели
    function getActiveFormId() {
        const selectors = [
            '.workitem-header-bar .work-item-form-id',
            '.work-item-form-id span',
            '.work-item-form-id',
            '.workitem-header-bar .info-text',
            '.work-item-form-header span',
            '.work-item-form-header',
            '.work-item-title-textfield',
            '.workitem-info-bar'
        ];

        for (const sel of selectors) {
            const elements = document.querySelectorAll(sel);
            for (const el of elements) {
                if (el.closest('.grid-canvas, .slick-viewport')) continue;
                const m = el.innerText.match(/\b\d{4,8}\b/);
                if (m) return m[0];
            }
        }

        const formLinks = document.querySelectorAll('.work-item-form a[href*="/_workitems/edit/"], .workitem-header-bar a[href*="/_workitems/edit/"]');
        for (const link of formLinks) {
            const m = link.href.match(/_workitems\/edit\/(\d{4,8})/);
            if (m) return m[1];
        }

        return null;
    }

    function isDiscussionRendered() {
        return !!document.querySelector('.comments-section, .discussion-messages-container, .work-item-discussion-control, .discussion-control, .work-item-comments');
    }

    function isFormLoading() {
        const spinners = document.querySelectorAll('.bowtie-spinner, .status-indicator, .workitem-form-loading, .form-loading');
        for (const s of spinners) {
            if (s.offsetParent !== null) return true;
        }
        return false;
    }

    function getCommentsSignature() {
        const comments = document.querySelectorAll('.comments-section .comment-item, .discussion-messages-container .comment-item, .work-item-comments .comment-item');
        if (!comments || comments.length === 0) return "EMPTY";
        const first = comments[0];
        const last = comments[comments.length - 1];
        return `${comments.length}_${first.getAttribute('data-comment-id') || ''}_${last.innerText.slice(0, 30)}`;
    }

    // Извлечение ТОЛЬКО последнего комментария (в TFS новые сверху)
    function extractComments() {
        const commentItems = document.querySelectorAll('.comments-section .comment-item, .discussion-messages-container .comment-item, .work-item-comments .comment-item');
        if (!commentItems || commentItems.length === 0) {
            return "Комментариев нет";
        }

        for (let i = 0; i < commentItems.length; i++) {
            const item = commentItems[i];
            const contentEl = item.querySelector('.comment-content, .message-content') || item;
            const text = contentEl.innerText.trim().replace(/\s+/g, ' ');
            if (text) {
                return text;
            }
        }

        return "Комментариев нет";
    }

    // Поиск реального контейнера прокрутки (правый вьюпорт в SlickGrid)
    function findScrollContainer() {
        // 1. Приоритет: .slick-viewport-right или вьюпорты с реальным скроллом
        const viewports = Array.from(document.querySelectorAll('.slick-viewport-right, .slick-viewport, .grid-viewport'));
        const scrollable = viewports.find(el => {
            const ov = window.getComputedStyle(el).overflowY;
            return el.scrollHeight > el.clientHeight + 10 && (ov === 'auto' || ov === 'scroll');
        });
        if (scrollable) return scrollable;

        // 2. Поиск родителя .grid-canvas с реальным overflow
        const canvases = document.querySelectorAll('.grid-canvas');
        for (const c of canvases) {
            let p = c.parentElement;
            while (p && p !== document.body) {
                if (p.scrollHeight > p.clientHeight + 10) {
                    const ov = window.getComputedStyle(p).overflowY;
                    if (ov === 'auto' || ov === 'scroll') return p;
                }
                p = p.parentElement;
            }
        }

        // 3. Fallback: контейнер с максимальным запасом скролла
        let best = null, maxDiff = 0;
        document.querySelectorAll('div').forEach(el => {
            const diff = el.scrollHeight - el.clientHeight;
            if (diff > maxDiff && el.clientHeight > 150) {
                const ov = window.getComputedStyle(el).overflowY;
                if (ov === 'auto' || ov === 'scroll') {
                    maxDiff = diff;
                    best = el;
                }
            }
        });
        return best;
    }

    try {
        const scrollContainer = findScrollContainer();
        if (!scrollContainer) throw new Error("Не найден скроллируемый контейнер таблицы TFS.");

        updateStatus("Перемотка списка наверх...");
        scrollContainer.scrollTop = 0;
        scrollContainer.dispatchEvent(new Event('scroll', { bubbles: true }));
        await new Promise(r => setTimeout(r, 800));

        let bottomStallCount = 0;

        while (!stopRequested) {
            // Собираем строки таблицы в DOM
            const domRows = Array.from(document.querySelectorAll('.grid-row'))
                .filter(r => r.style.display !== 'none')
                .sort((a, b) => a.offsetTop - b.offsetTop);

            for (const row of domRows) {
                if (stopRequested) break;

                const rowInfo = findRowInfo(row);
                if (!rowInfo) continue;

                const { id, clickable } = rowInfo;
                if (resultsMap.has(id)) continue;

                const prevSignature = getCommentsSignature();

                updateStatus(`[${resultsMap.size + 1}] Открываю #${id}...`);
                safeClick(clickable);

                // Ожидание смены ID в форме (до 4 сек)
                let formOpened = false;
                for (let i = 0; i < 20; i++) {
                    await new Promise(r => setTimeout(r, 200));
                    if (getActiveFormId() === id) {
                        formOpened = true;
                        break;
                    }
                }

                // Ожидание загрузки комментариев
                updateStatus(`[${resultsMap.size + 1}] Загрузка комментариев #${id}...`);
                for (let i = 0; i < 25; i++) {
                    await new Promise(r => setTimeout(r, 200));

                    if (isFormLoading()) continue;

                    const curSignature = getCommentsSignature();
                    const discussionReady = isDiscussionRendered();

                    if (curSignature !== prevSignature && curSignature !== "EMPTY") {
                        break;
                    }

                    if (discussionReady && formOpened && i >= 4) {
                        break;
                    }
                }

                await new Promise(r => setTimeout(r, 200));

                const comment = extractComments();
                resultsMap.set(id, comment);
                updateStatus(`Собрано: <b>${resultsMap.size}</b><br>Последняя: #${id} (${comment === "Комментариев нет" ? "без комм." : "комм. получен"})`);
            }

            // Проверка достижения реального конца списка
            const currentTop = scrollContainer.scrollTop;
            const maxScroll = scrollContainer.scrollHeight - scrollContainer.clientHeight;
            const isBottom = maxScroll > 0 && (maxScroll - currentTop <= 15);

            const visibleRowsWithIds = domRows.map(findRowInfo).filter(Boolean);
            const allVisibleProcessed = visibleRowsWithIds.length > 0 && visibleRowsWithIds.every(info => resultsMap.has(info.id));

            if (isBottom && allVisibleProcessed) {
                bottomStallCount++;
                if (bottomStallCount >= 3) {
                    break; // Действительно дошли до низа списка
                }
                await new Promise(r => setTimeout(r, 800));
            } else {
                bottomStallCount = 0;
            }

            // Шаг скролла: половина высоты видимой зоны
            const scrollStep = Math.max(160, Math.floor(scrollContainer.clientHeight * 0.5));
            const targetTop = Math.min(currentTop + scrollStep, maxScroll > 0 ? maxScroll : currentTop + scrollStep);

            updateStatus(`Собрано: <b>${resultsMap.size}</b>. Прокручиваю список дальше...`);
            scrollContainer.scrollTop = targetTop;
            scrollContainer.dispatchEvent(new Event('scroll', { bubbles: true }));

            // Синхронизируем левый вьюпорт, если он отдельный
            const leftViewport = document.querySelector('.slick-viewport-left');
            if (leftViewport && leftViewport !== scrollContainer) {
                leftViewport.scrollTop = targetTop;
                leftViewport.dispatchEvent(new Event('scroll', { bubbles: true }));
            }

            // Пауза, чтобы SlickGrid успел создать в DOM новые строки
            await new Promise(r => setTimeout(r, 600));
        }

        const finalData = Array.from(resultsMap.entries()).map(([number, comment]) => ({ number, comment }));
        updateStatus(`<b>Готово!</b> Собрано: ${finalData.length}. Скачиваю CSV...`);
        downloadCSV(finalData);
        setTimeout(() => overlay.remove(), 7000);

    } catch (err) {
        alert("Ошибка сбора TFS: " + err.message);
        if (overlay) overlay.remove();
    } finally {
        window._isTfsCollecting = false;
    }
})();