Your IP : 216.73.217.78


Current Path : /home/seto/indexator.pm/public/js/
Upload File :
Current File : /home/seto/indexator.pm/public/js/app.js

/* Indexator — app.js */

document.addEventListener('DOMContentLoaded', function () {

    // ── Bootstrap tooltips ──────────────────────────────────────────────────
    document.querySelectorAll('[data-bs-toggle="tooltip"]').forEach(function (el) {
        new bootstrap.Tooltip(el);
    });

    // ── Progress bar polling ─────────────────────────────────────────────────
    const progressBar = document.getElementById('indexator-progress-bar');
    if (progressBar) {
        const projectId     = progressBar.dataset.projectId;
        const pollInterval  = 3000; // ms
        let   timer         = null;

        // Badge HTML helpers (mirrors PHP statusBadge)
        function badgeHtml(status) {
            const map = {
                'indexed':     ['bg-success', 'indexed'],
                'not_indexed': ['bg-warning text-dark', 'not indexed'],
                'error':       ['bg-danger', 'error'],
                'pending':     ['bg-secondary', 'queued'],
            };
            const [cls, label] = map[status] || ['bg-light text-secondary border', '—'];
            return '<span class="badge ' + cls + '">' + label + '</span>';
        }

        function checkingBadgeHtml() {
            return '<span class="badge-checking">'
                + '<span class="spinner-border spinner-border-sm" style="width:.6rem;height:.6rem;border-width:2px"></span>'
                + ' checking</span>';
        }

        function updateProgress() {
            fetch('/api/projects/' + projectId + '/progress')
                .then(function (res) { return res.json(); })
                .then(function (data) {
                    const pct = data.percentage || 0;

                    // Update bar
                    progressBar.style.width = pct + '%';
                    progressBar.textContent = pct + '%';
                    progressBar.setAttribute('aria-valuenow', pct);

                    // Update stat counters if present
                    setCounter('stat-indexed',     data.indexed);
                    setCounter('stat-not-indexed', data.not_indexed);
                    setCounter('stat-error',       data.error);
                    setCounter('stat-pending',     data.pending);
                    setCounter('stat-checked-pct', pct + '%');

                    // ── Animate rows being checked ────────────────────────────
                    const pendingSet = new Set((data.pending_ids || []).map(Number));

                    // Show checking badge on pending rows
                    pendingSet.forEach(function (id) {
                        const cell = document.getElementById('status-' + id);
                        if (cell && !cell.dataset.checking) {
                            cell.innerHTML = checkingBadgeHtml();
                            cell.dataset.checking = '1';
                        }
                    });

                    // Update rows that were recently checked
                    (data.recent_updates || []).forEach(function (u) {
                        const cell = document.getElementById('status-' + u.id);
                        if (cell) {
                            cell.innerHTML = badgeHtml(u.status);
                            delete cell.dataset.checking;
                        }
                    });

                    // Remove checking animation from rows no longer pending
                    document.querySelectorAll('[data-checking]').forEach(function (cell) {
                        const id = Number(cell.id.replace('status-', ''));
                        if (!pendingSet.has(id)) {
                            // Status unknown until reload — show queued badge
                            cell.innerHTML = badgeHtml('pending');
                            delete cell.dataset.checking;
                        }
                    });

                    if (data.project_status !== 'checking') {
                        clearInterval(timer);
                        window.location.reload();
                    }
                })
                .catch(function () { /* silently ignore network errors */ });
        }

        timer = setInterval(updateProgress, pollInterval);
    }

    function setCounter(id, value) {
        const el = document.getElementById(id);
        if (el && value !== undefined) el.textContent = value;
    }

    // ── Recheck/Submit selected + select-all-across-pages ────────────────────
    const recheckSelectedBtn       = document.getElementById('recheck-selected-btn');
    const submitSelectedIndexingBtn = document.getElementById('submit-selected-indexing-btn');
    if (recheckSelectedBtn || submitSelectedIndexingBtn) {
        const urlCheckboxes      = document.querySelectorAll('.url-checkbox');
        const selectAllCheckbox  = document.getElementById('select-all-urls');
        const banner             = document.getElementById('select-all-pages-banner');
        const bannerText         = document.getElementById('select-all-pages-text');
        const selectAllPagesBtn  = document.getElementById('select-all-pages-btn');
        const clearAllBtn        = document.getElementById('clear-all-selection-btn');
        let   allPagesSelected   = false;

        function getCheckedCount() {
            return Array.from(urlCheckboxes).filter(cb => cb.checked).length;
        }

        function updateSelectionState() {
            const checkedCount = getCheckedCount();
            const anyChecked   = checkedCount > 0 || allPagesSelected;

            if (recheckSelectedBtn)        recheckSelectedBtn.disabled        = !anyChecked || allPagesSelected;
            if (submitSelectedIndexingBtn) submitSelectedIndexingBtn.disabled = !anyChecked;

            // show/hide banner
            if (banner) {
                const allOnPage = urlCheckboxes.length > 0 && checkedCount === urlCheckboxes.length;
                if (allPagesSelected) {
                    const total = submitSelectedIndexingBtn ? parseInt(submitSelectedIndexingBtn.dataset.total || '0', 10) : 0;
                    bannerText.textContent = 'All ' + total + ' URLs across all pages are selected.';
                    if (selectAllPagesBtn) selectAllPagesBtn.style.display = 'none';
                    banner.style.display = '';
                } else if (allOnPage && selectAllPagesBtn) {
                    bannerText.textContent = 'All ' + checkedCount + ' URLs on this page are selected.';
                    selectAllPagesBtn.style.display = '';
                    banner.style.display = '';
                } else {
                    banner.style.display = 'none';
                }
            }
        }

        urlCheckboxes.forEach(function (cb) {
            cb.addEventListener('change', function () {
                allPagesSelected = false;
                updateSelectionState();
            });
        });

        if (selectAllCheckbox) {
            selectAllCheckbox.addEventListener('change', function () {
                allPagesSelected = false;
                urlCheckboxes.forEach(cb => { cb.checked = selectAllCheckbox.checked; });
                updateSelectionState();
            });
        }

        if (selectAllPagesBtn) {
            selectAllPagesBtn.addEventListener('click', function () {
                allPagesSelected = true;
                updateSelectionState();
            });
        }

        if (clearAllBtn) {
            clearAllBtn.addEventListener('click', function () {
                allPagesSelected = false;
                urlCheckboxes.forEach(cb => { cb.checked = false; });
                if (selectAllCheckbox) selectAllCheckbox.checked = false;
                updateSelectionState();
            });
        }

        updateSelectionState();

        // Expose for the indexing modal
        window._indexingSelection = {
            getAllPagesSelected: function () { return allPagesSelected; },
            getCheckedIds: function () {
                return Array.from(urlCheckboxes)
                    .filter(cb => cb.checked)
                    .map(cb => cb.value);
            },
        };
    }

    // ── Stop all jobs (AJAX POST) ────────────────────────────────────────────
    const stopBtn = document.getElementById('stop-jobs-btn');
    if (stopBtn) {
        stopBtn.addEventListener('click', function () {
            if (!confirm('Остановить проверку? Все незавершённые задачи будут удалены.')) return;

            const projectId = stopBtn.dataset.projectId;
            const csrf      = document.querySelector('meta[name="csrf-token"]').content;

            stopBtn.disabled    = true;
            stopBtn.textContent = 'Stopping…';

            fetch('/api/projects/' + projectId + '/stop', {
                method: 'POST',
                headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
                body: '_csrf=' + encodeURIComponent(csrf),
            })
            .then(function (res) { return res.json(); })
            .then(function () {
                window.location.reload();
            })
            .catch(function () {
                alert('Не удалось остановить. Попробуйте ещё раз.');
                stopBtn.disabled    = false;
                stopBtn.textContent = '■ Stop';
            });
        });
    }

    // ── Recheck ALL projects (modal + AJAX POST) ─────────────────────────────
    const recheckAllBtn = document.getElementById('recheck-all-btn');
    if (recheckAllBtn) {
        const recheckAllModalEl = document.getElementById('recheckAllModal');
        const recheckAllModal   = recheckAllModalEl ? new bootstrap.Modal(recheckAllModalEl) : null;

        recheckAllBtn.addEventListener('click', function () {
            if (recheckAllModal) {
                recheckAllModal.show();
            }
        });

        if (recheckAllModalEl) {
            recheckAllModalEl.addEventListener('click', function (e) {
                const btn = e.target.closest('.recheck-all-confirm');
                if (!btn) return;

                const scope = btn.dataset.scope;
                const csrf  = recheckAllBtn.dataset.csrf;

                recheckAllModal.hide();
                recheckAllBtn.disabled  = true;
                recheckAllBtn.innerHTML = '<i class="bi bi-arrow-clockwise"></i> Starting…';

                fetch('/api/projects/recheck-all', {
                    method: 'POST',
                    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
                    body: '_csrf=' + encodeURIComponent(csrf) + '&scope=' + encodeURIComponent(scope),
                })
                .then(function (res) { return res.json(); })
                .then(function () { window.location.reload(); })
                .catch(function () {
                    alert('Не удалось запустить. Попробуйте ещё раз.');
                    recheckAllBtn.disabled  = false;
                    recheckAllBtn.innerHTML = '<i class="bi bi-arrow-clockwise"></i> Recheck All';
                });
            });
        }
    }

    // ── Recheck single project (AJAX POST) ──────────────────────────────────
    document.addEventListener('click', function (e) {
        const btn = e.target.closest('.btn-recheck-project');
        if (!btn) return;

        const projectId = btn.dataset.projectId;
        const csrf      = document.querySelector('meta[name="csrf-token"]').content;

        btn.disabled = true;
        btn.querySelector('i').className = 'bi bi-arrow-clockwise spin';

        fetch('/api/projects/' + projectId + '/recheck', {
            method: 'POST',
            headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
            body: '_csrf=' + encodeURIComponent(csrf),
        })
        .then(function (res) { return res.json(); })
        .then(function (data) {
            if (data.error) {
                alert(data.error);
                btn.disabled = false;
                btn.querySelector('i').className = 'bi bi-arrow-clockwise';
            } else {
                window.location.reload();
            }
        })
        .catch(function () {
            alert('Не удалось запустить. Попробуйте ещё раз.');
            btn.disabled = false;
            btn.querySelector('i').className = 'bi bi-arrow-clockwise';
        });
    });

    // ── Stop ALL projects (AJAX POST) ───────────────────────────────────────
    const stopAllBtn = document.getElementById('stop-all-btn');
    if (stopAllBtn) {
        stopAllBtn.addEventListener('click', function () {
            if (!confirm('Остановить проверку по всем проектам? Все незавершённые задачи будут удалены.')) return;

            const csrf = stopAllBtn.dataset.csrf;
            stopAllBtn.disabled    = true;
            stopAllBtn.textContent = 'Stopping…';

            fetch('/api/projects/stop-all', {
                method: 'POST',
                headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
                body: '_csrf=' + encodeURIComponent(csrf),
            })
            .then(function (res) { return res.json(); })
            .then(function () { window.location.reload(); })
            .catch(function () {
                alert('Не удалось остановить. Попробуйте ещё раз.');
                stopAllBtn.disabled    = false;
                stopAllBtn.textContent = '■ Stop All';
            });
        });
    }

    // ── Reset stuck jobs (AJAX POST) ─────────────────────────────────────────
    const resetBtn = document.getElementById('reset-stuck-jobs-btn');
    if (resetBtn) {
        resetBtn.addEventListener('click', function () {
            const projectId = resetBtn.dataset.projectId;
            const csrf      = document.querySelector('meta[name="csrf-token"]').content;

            fetch('/api/projects/' + projectId + '/reset-jobs', {
                method: 'POST',
                headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
                body: '_csrf=' + encodeURIComponent(csrf),
            })
            .then(function (res) { return res.json(); })
            .then(function (data) {
                alert('Reset ' + data.reset + ' stuck job(s). The page will reload.');
                window.location.reload();
            })
            .catch(function () {
                alert('Failed to reset jobs. Please try again.');
            });
        });
    }

    // ── Indexing submit modal ─────────────────────────────────────────────────
    const indexingModalEl = document.getElementById('indexingModal');
    if (indexingModalEl) {
        const indexingModal  = new bootstrap.Modal(indexingModalEl);
        const submitBtn      = document.getElementById('indexing-submit-btn');
        const resultDiv      = document.getElementById('indexing-result');
        const warningDiv     = document.getElementById('indexing-warning');
        const urlCountEl     = document.getElementById('indexing-url-count');
        const urlLabelEl     = document.getElementById('indexing-url-label');
        let   currentProject = null; // { id, name, mode, urlIds }
        const csrf           = document.querySelector('meta[name="csrf-token"]').content;

        function openIndexingModal(projectId, projectName, mode, urlIds, urlCount) {
            currentProject = { id: projectId, name: projectName, mode: mode, urlIds: urlIds || [] };

            document.getElementById('indexing-project-name').textContent = projectName;
            if (urlCountEl) urlCountEl.textContent = urlCount !== undefined ? urlCount : '…';
            if (urlLabelEl) {
                urlLabelEl.textContent = mode === 'all'      ? '(all URLs)'
                                       : mode === 'selected' ? '(selected)'
                                       : '(not indexed)';
            }
            warningDiv.style.display = 'none';
            resultDiv.style.display  = 'none';
            submitBtn.disabled       = false;
            submitBtn.textContent    = '⚡ Submit';
            submitBtn.onclick        = null;

            // Check previous submission
            fetch('/api/projects/' + projectId + '/indexing')
                .then(function (r) { return r.json(); })
                .then(function (data) {
                    if (data.last) {
                        const d    = new Date(data.last.created_at.replace(' ', 'T') + 'Z');
                        const days = Math.round((Date.now() - d) / 86400000);
                        const ago  = days === 0 ? 'today' : days + 'd ago';
                        warningDiv.innerHTML = '⚠ Already submitted <strong>' + data.last.urls_count
                            + ' URLs</strong> ' + ago + ' (' + data.last.engines.toUpperCase()
                            + ') — status: <strong>' + data.last.status + '</strong>. Submit again?';
                        warningDiv.style.display = '';
                    }
                })
                .catch(function () {});

            indexingModal.show();
        }

        // ⚡ Submit to index (not_indexed mode — existing button)
        document.addEventListener('click', function (e) {
            const btn = e.target.closest('.btn-index-project');
            if (!btn) return;
            openIndexingModal(
                btn.dataset.projectId,
                btn.dataset.projectName,
                'not_indexed',
                [],
                parseInt(btn.dataset.notIndexed || '0', 10)
            );
        });

        // ⚡ Submit Selected to Index
        const submitSelectedBtn = document.getElementById('submit-selected-indexing-btn');
        if (submitSelectedBtn) {
            submitSelectedBtn.addEventListener('click', function () {
                const sel      = window._indexingSelection;
                const allPages = sel && sel.getAllPagesSelected();
                const ids      = sel && !allPages ? sel.getCheckedIds() : [];
                const mode     = allPages ? 'all' : 'selected';
                const count    = allPages
                    ? parseInt(submitSelectedBtn.dataset.total || '0', 10)
                    : ids.length;
                openIndexingModal(
                    submitSelectedBtn.dataset.projectId,
                    submitSelectedBtn.dataset.projectName,
                    mode,
                    ids,
                    count
                );
            });
        }

        submitBtn.addEventListener('click', function () {
            if (!currentProject) return;

            const engines = [];
            if (document.getElementById('engine-google').checked) engines.push('google');
            if (document.getElementById('engine-bing').checked)   engines.push('bing');

            if (engines.length === 0) {
                resultDiv.innerHTML     = '<div class="alert alert-warning mb-0">Select at least one engine.</div>';
                resultDiv.style.display = '';
                return;
            }

            submitBtn.disabled      = true;
            submitBtn.textContent   = 'Submitting…';
            resultDiv.style.display = 'none';

            let body = '_csrf=' + encodeURIComponent(csrf)
                + '&mode=' + encodeURIComponent(currentProject.mode)
                + engines.map(function (e) { return '&engines[]=' + e; }).join('');

            if (currentProject.mode === 'selected' && currentProject.urlIds.length > 0) {
                body += currentProject.urlIds.map(function (id) { return '&url_ids[]=' + encodeURIComponent(id); }).join('');
            }

            fetch('/api/projects/' + currentProject.id + '/submit-indexing', {
                method:  'POST',
                headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
                body:    body,
            })
            .then(function (r) { return r.json(); })
            .then(function (data) {
                if (data.error) {
                    resultDiv.innerHTML = '<div class="alert alert-danger mb-0">' + data.error + '</div>';
                } else {
                    const errs  = data.errors && data.errors.length
                        ? '<br><small class="text-warning">Warnings: ' + data.errors.join('; ') + '</small>'
                        : '';
                    resultDiv.innerHTML = '<div class="alert alert-success mb-0">'
                        + '✓ Submitted <strong>' + data.urls_count + ' URLs</strong>'
                        + ' in <strong>' + data.batch_count + ' batch(es)</strong>'
                        + ' to <strong>' + data.engines.join(', ').toUpperCase() + '</strong>.'
                        + '<br><small class="text-muted">Task IDs: ' + data.task_ids.join(', ') + '</small>'
                        + errs
                        + '</div>';
                    submitBtn.textContent    = '✓ Done';
                    submitBtn.disabled       = false;
                    submitBtn.onclick        = function () { indexingModal.hide(); };
                    warningDiv.style.display = 'none';
                }
                resultDiv.style.display = '';
            })
            .catch(function () {
                resultDiv.innerHTML     = '<div class="alert alert-danger mb-0">Request failed.</div>';
                resultDiv.style.display = '';
                submitBtn.disabled      = false;
                submitBtn.textContent   = '⚡ Submit';
            });
        });
    }

    // ── Refresh indexing submission status ───────────────────────────────────
    document.addEventListener('click', function (e) {
        const btn = e.target.closest('.refresh-indexing-btn');
        if (!btn) return;

        const submissionId = btn.dataset.submissionId;
        const csrf         = document.querySelector('meta[name="csrf-token"]').content;
        btn.disabled       = true;
        btn.textContent    = '…';

        fetch('/api/indexing/' + submissionId + '/refresh', {
            method:  'POST',
            headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
            body:    '_csrf=' + encodeURIComponent(csrf),
        })
        .then(function (r) { return r.json(); })
        .then(function (data) {
            btn.textContent = data.status || 'done';
            btn.disabled    = false;
            // Update badge in row
            const row    = btn.closest('tr');
            const badge  = row ? row.querySelector('.badge') : null;
            if (badge && data.status) {
                const cls = { complete: 'bg-success', failed: 'bg-danger', partial: 'bg-warning text-dark' };
                badge.className = 'badge ' + (cls[data.status] || 'bg-secondary');
                badge.textContent = data.status;
            }
        })
        .catch(function () {
            btn.textContent = '↻ Refresh';
            btn.disabled    = false;
        });
    });

    // ── Check IndexingBot balance (settings page) ────────────────────────────
    const checkBalanceBtn = document.getElementById('check-balance-btn');
    if (checkBalanceBtn) {
        checkBalanceBtn.addEventListener('click', function () {
            const resultEl = document.getElementById('balance-result');
            checkBalanceBtn.disabled    = true;
            checkBalanceBtn.textContent = '…';

            fetch('/api/indexing/balance')
                .then(function (r) { return r.json(); })
                .then(function (data) {
                    if (data.error || data.status >= 400) {
                        resultEl.className   = 'alert alert-danger py-2 small mb-3';
                        resultEl.textContent = 'Error: ' + (data.error || data.msg);
                    } else {
                        const balance    = parseFloat(data.data.balance    || 0).toFixed(2);
                        const balanceRef = parseFloat(data.data.balance_ref || 0).toFixed(2);
                        resultEl.className = 'alert alert-success py-2 small mb-3';
                        resultEl.textContent = 'Баланс: ' + balance + ' ₽'
                            + (balanceRef > 0 ? ' + ' + balanceRef + ' ₽ (реф.)' : '');
                    }
                    resultEl.style.display = '';
                })
                .catch(function () {
                    resultEl.className     = 'alert alert-danger py-2 small mb-3';
                    resultEl.textContent   = 'Request failed.';
                    resultEl.style.display = '';
                })
                .finally(function () {
                    checkBalanceBtn.disabled    = false;
                    checkBalanceBtn.textContent = 'Check balance';
                });
        });
    }

    // ── Project sort ─────────────────────────────────────────────────────────
    const sortSelect = document.getElementById('project-sort');
    if (sortSelect) {
        const projectsList = document.getElementById('projects-list');
        const SORT_KEY = 'indexator_project_sort';

        function sortCards(value) {
            const cards = Array.from(projectsList.querySelectorAll('.project-card'));

            cards.sort(function (a, b) {
                switch (value) {
                    case 'name_asc':
                        return a.dataset.name.localeCompare(b.dataset.name);
                    case 'name_desc':
                        return b.dataset.name.localeCompare(a.dataset.name);
                    case 'total_desc':
                        return parseInt(b.dataset.total) - parseInt(a.dataset.total);
                    case 'rate_asc':
                        return parseInt(a.dataset.rate) - parseInt(b.dataset.rate);
                    case 'created_desc':
                        return parseInt(b.dataset.created) - parseInt(a.dataset.created);
                    case 'submitted_desc': {
                        const ta = parseInt(a.dataset.submitted);
                        const tb = parseInt(b.dataset.submitted);
                        // Never-submitted go to the end
                        if (ta === 0 && tb === 0) return 0;
                        if (ta === 0) return 1;
                        if (tb === 0) return -1;
                        return tb - ta;
                    }
                    default:
                        return 0;
                }
            });

            cards.forEach(function (c) { projectsList.appendChild(c); });
        }

        // Restore saved sort
        const saved = localStorage.getItem(SORT_KEY);
        if (saved) {
            sortSelect.value = saved;
        }
        sortCards(sortSelect.value);

        sortSelect.addEventListener('change', function () {
            localStorage.setItem(SORT_KEY, this.value);
            sortCards(this.value);
        });
    }

    // ── Project search ───────────────────────────────────────────────────────
    const searchInput = document.getElementById('project-search');
    if (searchInput) {
        const summaryCard = document.getElementById('summary-card');
        const cards       = document.querySelectorAll('.project-card');
        const noResults   = document.getElementById('no-search-results');

        searchInput.addEventListener('input', function () {
            const q = this.value.trim().toLowerCase();

            if (q === '') {
                cards.forEach(function (c) { c.style.display = ''; });
                if (summaryCard) summaryCard.style.display = '';
                noResults.style.display = 'none';
                return;
            }

            if (summaryCard) summaryCard.style.display = 'none';

            let visible = 0;
            cards.forEach(function (c) {
                const match = c.dataset.name.includes(q) || c.dataset.domain.includes(q);
                c.style.display = match ? '' : 'none';
                if (match) visible++;
            });

            noResults.style.display = visible === 0 ? '' : 'none';
        });
    }
});