Comments Table + Export Excel

Show mt-comments data as table and export to Excel

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(Tôi đã có Trình quản lý tập lệnh người dùng, hãy cài đặt nó!)

Advertisement:

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

Advertisement:

// ==UserScript==
// @name         Comments Table + Export Excel
// @namespace    https://nguyengiathieu.lms.vnedu.vn/*
// @version      1.0
// @description  Show mt-comments data as table and export to Excel
// @author       You
// @match        https://nguyengiathieu.lms.vnedu.vn/*
// @license MIT
// @grant        none
// ==/UserScript==



(function () {
    'use strict';

    const PANEL_ID = 'tm-comment-table-box';
    const FLOATING_BTN_ID = 'tm-show-comments-btn';

    function normalizeText(text) {
        return String(text || '')
            .replace(/\s+/g, ' ')
            .replace(/ /g, ' ')
            .trim()
            .toUpperCase();
    }

    function findPortletByCaption(captionText) {
        const portlets = document.querySelectorAll('.portlet');

        for (const portlet of portlets) {
            const caption = portlet.querySelector('.portlet-title .caption');

            if (!caption) continue;

            const captionContent = normalizeText(caption.innerText);

            if (captionContent.includes(normalizeText(captionText))) {
                return portlet;
            }
        }

        return null;
    }

    function pad2(number) {
        return String(number).padStart(2, '0');
    }

    function formatDateTime(date) {
        return `${pad2(date.getDate())}/${pad2(date.getMonth() + 1)}/${date.getFullYear()} ${pad2(date.getHours())}:${pad2(date.getMinutes())}`;
    }

    function convertRelativeTimeToDateTime(relativeTime) {
        const now = new Date();
        const text = String(relativeTime || '').toLowerCase().trim();

        const resultDate = new Date(now);

        if (
            text.includes('vừa xong') ||
            text.includes('just now') ||
            text.includes('now')
        ) {
            return formatDateTime(resultDate);
        }

        const numberMatch = text.match(/\d+/);
        const value = numberMatch ? parseInt(numberMatch[0], 10) : 0;

        if (text.includes('phút')) {
            resultDate.setMinutes(resultDate.getMinutes() - value);
        } else if (text.includes('giờ')) {
            resultDate.setHours(resultDate.getHours() - value);
        } else if (text.includes('ngày')) {
            resultDate.setDate(resultDate.getDate() - value);
        } else if (text.includes('tuần')) {
            resultDate.setDate(resultDate.getDate() - value * 7);
        } else if (text.includes('tháng')) {
            resultDate.setMonth(resultDate.getMonth() - value);
        } else if (text.includes('năm')) {
            resultDate.setFullYear(resultDate.getFullYear() - value);
        } else {
            return relativeTime;
        }

        return formatDateTime(resultDate);
    }

    function extractComments() {
        const targetPortlet = findPortletByCaption('Ý KIẾN KHÁC');

        if (!targetPortlet) {
            alert('Không tìm thấy block có caption: Ý KIẾN KHÁC');
            return [];
        }

        const comments = targetPortlet.querySelectorAll('.portlet-body .mt-comments .mt-comment');

        return Array.from(comments).map((item, index) => {
            const author = item.querySelector('.mt-comment-author')?.innerText.trim() || '';

            const rawDate = item.querySelector('.mt-comment-date')?.innerText.trim() || '';
            const calculatedDate = convertRelativeTimeToDateTime(rawDate);

            const text = item.querySelector('.mt-comment-text')?.innerText.trim() || '';

            return {
                STT: index + 1,
                MaPhuHuynh: author,
                ThoiGian: calculatedDate,
                YKien: text
            };
        });
    }

    function injectStyle() {
        if (document.getElementById('tm-ykk-style')) return;

        const style = document.createElement('style');
        style.id = 'tm-ykk-style';

        style.innerHTML = `
            #${PANEL_ID} {
                position: fixed;
                top: 72px;
                right: 24px;
                width: 820px;
                max-width: calc(100vw - 48px);
                max-height: 82vh;
                background: #ffffff;
                border-radius: 14px;
                z-index: 999999;
                box-shadow: 0 18px 45px rgba(15, 23, 42, 0.22);
                font-family: Arial, Roboto, "Segoe UI", sans-serif;
                overflow: hidden;
                color: #1f2937;
                border: 1px solid #dbe3ef;
            }

            .tm-ykk-header {
                display: flex;
                align-items: center;
                justify-content: space-between;
                gap: 14px;
                padding: 14px 16px;
                background: linear-gradient(135deg, #1e3a8a, #2563eb);
                color: #ffffff;
                cursor: pointer;
                user-select: none;
            }

            .tm-ykk-title-wrap {
                display: flex;
                align-items: center;
                gap: 10px;
                min-width: 0;
            }

            .tm-ykk-icon {
                width: 34px;
                height: 34px;
                border-radius: 10px;
                display: flex;
                align-items: center;
                justify-content: center;
                background: rgba(255, 255, 255, 0.18);
                font-size: 18px;
                flex-shrink: 0;
            }

            .tm-ykk-title {
                display: flex;
                flex-direction: column;
                gap: 2px;
                min-width: 0;
            }

            .tm-ykk-title strong {
                font-size: 15px;
                font-weight: 700;
                letter-spacing: 0.2px;
                white-space: nowrap;
                overflow: hidden;
                text-overflow: ellipsis;
            }

            .tm-ykk-title span {
                font-size: 12px;
                color: rgba(255, 255, 255, 0.82);
            }

            .tm-ykk-actions {
                display: flex;
                align-items: center;
                gap: 8px;
                flex-shrink: 0;
            }

            .tm-ykk-badge {
                display: inline-flex;
                align-items: center;
                justify-content: center;
                min-width: 34px;
                height: 26px;
                padding: 0 10px;
                border-radius: 999px;
                background: rgba(255, 255, 255, 0.18);
                color: #ffffff;
                font-size: 12px;
                font-weight: 700;
            }

            .tm-ykk-btn {
                border: none;
                border-radius: 9px;
                padding: 8px 12px;
                cursor: pointer;
                font-size: 12px;
                font-weight: 700;
                transition: 0.15s ease;
                color: #ffffff;
                font-family: Arial, Roboto, "Segoe UI", sans-serif;
            }

            .tm-ykk-btn:hover {
                filter: brightness(1.08);
            }

            .tm-ykk-btn-download {
                background: #16a34a;
            }

            .tm-ykk-btn-close {
                background: #ef4444;
            }

            .tm-ykk-collapse-indicator {
                width: 28px;
                height: 28px;
                border-radius: 9px;
                display: flex;
                align-items: center;
                justify-content: center;
                background: rgba(255, 255, 255, 0.16);
                font-size: 14px;
                font-weight: 700;
            }

            .tm-ykk-body {
                background: #f8fafc;
                padding: 14px;
            }

            .tm-ykk-toolbar {
                display: flex;
                align-items: center;
                justify-content: space-between;
                margin-bottom: 12px;
                gap: 12px;
            }

            .tm-ykk-note {
                font-size: 12px;
                color: #64748b;
            }

            .tm-ykk-search {
                width: 280px;
                max-width: 100%;
                border: 1px solid #cbd5e1;
                background: #ffffff;
                border-radius: 10px;
                padding: 9px 11px;
                font-size: 13px;
                outline: none;
                font-family: Arial, Roboto, "Segoe UI", sans-serif;
            }

            .tm-ykk-search:focus {
                border-color: #2563eb;
                box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.12);
            }

            .tm-ykk-table-wrap {
                max-height: calc(82vh - 150px);
                overflow: auto;
                background: #ffffff;
                border-radius: 12px;
                border: 1px solid #e2e8f0;
            }

            .tm-ykk-table {
                width: 100%;
                border-collapse: separate;
                border-spacing: 0;
                font-size: 13px;
                font-family: Arial, Roboto, "Segoe UI", sans-serif;
            }

            .tm-ykk-table thead th {
                position: sticky;
                top: 0;
                z-index: 2;
                background: #eaf1ff;
                color: #1e293b;
                font-weight: 700;
                text-align: left;
                padding: 11px 10px;
                border-bottom: 1px solid #cbd5e1;
                white-space: nowrap;
            }

            .tm-ykk-table tbody td {
                padding: 10px;
                border-bottom: 1px solid #e5e7eb;
                vertical-align: top;
                color: #334155;
            }

            .tm-ykk-table tbody tr:nth-child(even) {
                background: #f9fafb;
            }

            .tm-ykk-table tbody tr:hover {
                background: #eff6ff;
            }

            .tm-ykk-table tbody tr:last-child td {
                border-bottom: none;
            }

            .tm-ykk-center {
                text-align: center;
            }

            .tm-ykk-code {
                font-weight: 700;
                color: #1d4ed8;
                white-space: nowrap;
            }

            .tm-ykk-time {
                white-space: nowrap;
                color: #475569;
            }

            .tm-ykk-text {
                white-space: pre-wrap;
                line-height: 1.5;
                min-width: 300px;
                color: #1f2937;
            }

            #${FLOATING_BTN_ID} {
                position: fixed;
                bottom: 30px;
                right: 30px;
                z-index: 999999;
                background: linear-gradient(135deg, #2563eb, #1e3a8a);
                color: white;
                border: none;
                padding: 12px 16px;
                border-radius: 999px;
                cursor: pointer;
                font-size: 14px;
                font-weight: 700;
                font-family: Arial, Roboto, "Segoe UI", sans-serif;
                box-shadow: 0 12px 28px rgba(37, 99, 235, 0.35);
                transition: 0.15s ease;
            }

            #${FLOATING_BTN_ID}:hover {
                transform: translateY(-2px);
                box-shadow: 0 16px 34px rgba(37, 99, 235, 0.42);
            }

            @media (max-width: 768px) {
                #${PANEL_ID} {
                    top: 20px;
                    right: 12px;
                    left: 12px;
                    width: auto;
                    max-width: none;
                }

                .tm-ykk-header {
                    align-items: flex-start;
                }

                .tm-ykk-actions {
                    flex-wrap: wrap;
                    justify-content: flex-end;
                }

                .tm-ykk-toolbar {
                    flex-direction: column;
                    align-items: stretch;
                }

                .tm-ykk-search {
                    width: 100%;
                }
            }
        `;

        document.head.appendChild(style);
    }

    function hideFloatingButton() {
        const btn = document.getElementById(FLOATING_BTN_ID);
        if (btn) {
            btn.style.display = 'none';
        }
    }

    function showFloatingButton() {
        const btn = document.getElementById(FLOATING_BTN_ID);
        if (btn) {
            btn.style.display = 'block';
        }
    }

    function closePanelToButton() {
        const panel = document.getElementById(PANEL_ID);
        if (panel) {
            panel.remove();
        }

        showFloatingButton();
    }

    function createUI(data) {
        injectStyle();

        const oldBox = document.getElementById(PANEL_ID);
        if (oldBox) oldBox.remove();

        hideFloatingButton();

        const box = document.createElement('div');
        box.id = PANEL_ID;

        box.innerHTML = `
            <div class="tm-ykk-header" id="tm-ykk-header">
                <div class="tm-ykk-title-wrap">
                    <div class="tm-ykk-icon">💬</div>

                    <div class="tm-ykk-title">
                        <strong>Ý KIẾN KHÁC</strong>
                        <span>Click vào thanh này để thu gọn về nút ban đầu</span>
                    </div>
                </div>

                <div class="tm-ykk-actions">
                    <span class="tm-ykk-badge">${data.length}</span>

                    <button class="tm-ykk-btn tm-ykk-btn-download" id="tm-download-excel" title="Tải Excel">
                        Tải Excel
                    </button>

                    <button class="tm-ykk-btn tm-ykk-btn-close" id="tm-close-table" title="Đóng">
                        Đóng
                    </button>

                    <span class="tm-ykk-collapse-indicator">▼</span>
                </div>
            </div>

            <div class="tm-ykk-body">
                <div class="tm-ykk-toolbar">
                    <div class="tm-ykk-note">
                        Chỉ hiển thị dữ liệu trong block <b>Ý KIẾN KHÁC</b>.
                    </div>

                    <input
                        id="tm-ykk-search"
                        class="tm-ykk-search"
                        type="text"
                        placeholder="Tìm theo mã PH hoặc nội dung..."
                    />
                </div>

                <div class="tm-ykk-table-wrap">
                    <table class="tm-ykk-table">
                        <thead>
                            <tr>
                                <th style="width:52px;">STT</th>
                                <th style="width:120px;">Mã PH</th>
                                <th style="width:155px;">Thời gian</th>
                                <th>Ý kiến</th>
                            </tr>
                        </thead>

                        <tbody id="tm-ykk-table-body">
                            ${renderRows(data)}
                        </tbody>
                    </table>
                </div>
            </div>
        `;

        document.body.appendChild(box);

        const header = document.getElementById('tm-ykk-header');
        const closeBtn = document.getElementById('tm-close-table');
        const downloadBtn = document.getElementById('tm-download-excel');
        const searchInput = document.getElementById('tm-ykk-search');

        header.addEventListener('click', () => {
            closePanelToButton();
        });

        closeBtn.addEventListener('click', (event) => {
            event.stopPropagation();
            closePanelToButton();
        });

        downloadBtn.addEventListener('click', (event) => {
            event.stopPropagation();
            downloadExcel(data);
        });

        searchInput.addEventListener('click', (event) => {
            event.stopPropagation();
        });

        searchInput.addEventListener('input', () => {
            const keyword = normalizeText(searchInput.value);

            const filteredData = data.filter(row => {
                return (
                    normalizeText(row.MaPhuHuynh).includes(keyword) ||
                    normalizeText(row.ThoiGian).includes(keyword) ||
                    normalizeText(row.YKien).includes(keyword)
                );
            });

            document.getElementById('tm-ykk-table-body').innerHTML = renderRows(filteredData);
        });
    }

    function renderRows(data) {
        if (!data.length) {
            return `
                <tr>
                    <td colspan="4" class="tm-ykk-center" style="padding:18px;color:#64748b;">
                        Không tìm thấy dữ liệu phù hợp.
                    </td>
                </tr>
            `;
        }

        return data.map(row => `
            <tr>
                <td class="tm-ykk-center">${escapeHtml(row.STT)}</td>
                <td class="tm-ykk-code">${escapeHtml(row.MaPhuHuynh)}</td>
                <td class="tm-ykk-time">${escapeHtml(row.ThoiGian)}</td>
                <td class="tm-ykk-text">${escapeHtml(row.YKien)}</td>
            </tr>
        `).join('');
    }

    function downloadExcel(data) {
        const html = `
        <html>
        <head>
            <meta charset="UTF-8">
        </head>
        <body>
            <table border="1">
                <thead>
                    <tr>
                        <th>STT</th>
                        <th>Mã phụ huynh</th>
                        <th>Thời gian</th>
                        <th>Ý kiến</th>
                    </tr>
                </thead>
                <tbody>
                    ${data.map(row => `
                        <tr>
                            <td>${escapeHtml(row.STT)}</td>
                            <td>${escapeHtml(row.MaPhuHuynh)}</td>
                            <td>${escapeHtml(row.ThoiGian)}</td>
                            <td>${escapeHtml(row.YKien)}</td>
                        </tr>
                    `).join('')}
                </tbody>
            </table>
        </body>
        </html>
    `;

        const fileName = `y_kien_khac_${new Date().toISOString().slice(0, 10)}.xls`;

        const a = document.createElement('a');

        a.href = 'data:application/vnd.ms-excel;charset=utf-8,' + encodeURIComponent(html);
        a.download = fileName;
        a.style.display = 'none';

        document.body.appendChild(a);
        a.click();
        document.body.removeChild(a);
    }
    function escapeHtml(value) {
        return String(value ?? '')
            .replace(/&/g, '&amp;')
            .replace(/</g, '&lt;')
            .replace(/>/g, '&gt;')
            .replace(/"/g, '&quot;')
            .replace(/'/g, '&#039;');
    }

    function addFloatingButton() {
        injectStyle();

        if (document.getElementById(FLOATING_BTN_ID)) return;

        const btn = document.createElement('button');
        btn.id = FLOATING_BTN_ID;
        btn.innerText = '💬 Show Ý KIẾN KHÁC';

        btn.addEventListener('click', () => {
            const data = extractComments();

            if (data.length === 0) {
                alert('Không có dữ liệu trong block Ý KIẾN KHÁC.');
                return;
            }

            createUI(data);
        });

        document.body.appendChild(btn);
    }

    window.addEventListener('load', () => {
        setTimeout(addFloatingButton, 1000);
    });
})();