Comments Table + Export Excel

Show mt-comments data as table and export to Excel

スクリプトをインストールするには、Tampermonkey, GreasemonkeyViolentmonkey のような拡張機能のインストールが必要です。

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

スクリプトをインストールするには、TampermonkeyViolentmonkey のような拡張機能のインストールが必要です。

スクリプトをインストールするには、TampermonkeyUserscripts のような拡張機能のインストールが必要です。

このスクリプトをインストールするには、Tampermonkeyなどの拡張機能をインストールする必要があります。

このスクリプトをインストールするには、ユーザースクリプト管理ツールの拡張機能をインストールする必要があります。

(ユーザースクリプト管理ツールは設定済みなのでインストール!)

Advertisement:

このスタイルをインストールするには、Stylusなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus などの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus tなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

(ユーザースタイル管理ツールは設定済みなのでインストール!)

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);
    });
})();