FRIDA SV IPB - But BETTER UX

Current UX sucks, use this script instead

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         FRIDA SV IPB - But BETTER UX
// @namespace    Violentmonkey Scripts
// @version      67.6.9
// @description  Current UX sucks, use this script instead
// @author       opqdul
// @license MIT
// @match        https://fridasv.com/mahasiswa/jadwal-kolokium*
// @match        https://fridasv.com/mahasiswa/jadwal-seminar*
// @match        https://fridasv.com/mahasiswa/kartu-kolokium*
// @match        https://fridasv.com/mahasiswa/kartu-seminar*
// @icon         https://fridasv.com/images/logo-form2.jpg
// @grant        GM_setClipboard
// @grant        GM_notification
// @grant        GM_xmlhttpRequest
// @connect      fridasv.com
// @run-at       document-end
// ==/UserScript==

(function() {
    'use strict';

    // Detect current page
    const currentURL = window.location.href;
    const isJadwalKolokium = currentURL.includes('jadwal-kolokium') && !currentURL.includes('kartu-kolokium');
    const isJadwalSeminar = currentURL.includes('jadwal-seminar') && !currentURL.includes('kartu-seminar');
    const isKartuKolokium = currentURL.includes('kartu-kolokium');
    const isKartuSeminar = currentURL.includes('kartu-seminar');

    if (!isJadwalKolokium && !isJadwalSeminar && !isKartuKolokium && !isKartuSeminar) {
        console.log('[FRIDA Enhancer] Not a target page, exiting...');
        return;
    }

    // Global state
    const CHUNK_SIZE = 200;
    const PRELOAD_THRESHOLD = 50;
    let allDataCache = [];
    let totalRecords = 0;
    let dataTableInstance = null;
    let isLoadingMore = false;
    let currentEndpoint = '';
    let currentCsrfToken = '';

    // Inject CSS
    injectCustomCSS();

    // Route to appropriate handler
    if (isKartuKolokium || isKartuSeminar) {
        const kartuType = isKartuKolokium ? 'Kolokium' : 'Seminar';
        handleKartu(kartuType);
    } else {
        const pageType = isJadwalKolokium ? 'Kolokium' : 'Seminar';
        const endpoint = isJadwalKolokium ? 'jadwal-kolokium' : 'jadwal-seminar';
        handleJadwal(pageType, endpoint);
    }

    // ============================================
    // KARTU HANDLER
    // ============================================
    function handleKartu(type) {
        console.log(`[FRIDA Enhancer] Kartu ${type} page detected`);

        const waitForReady = setInterval(() => {
            if (typeof jQuery !== 'undefined' && typeof jQuery.fn.DataTable !== 'undefined') {
                clearInterval(waitForReady);
                setTimeout(() => enhanceKartu(type), 1000);
            }
        }, 100);
    }

    function enhanceKartu(type) {
        const $table = jQuery('#datatable1');

        if ($table.length === 0) {
            console.error('[FRIDA Enhancer] Kartu table not found!');
            return;
        }

        console.log(`[FRIDA Enhancer] Enhancing Kartu ${type}...`);

        if (jQuery.fn.DataTable.isDataTable('#datatable1')) {
            $table.DataTable().destroy();
        }

        dataTableInstance = $table.DataTable({
            "responsive": true,
            "lengthChange": true,
            "aLengthMenu": [
                [10, 25, 50, 75, -1],
                [10, 25, 50, 75, "All"]
            ],
            "pageLength": 10,
            "autoWidth": false,
            "pagingType": "full_numbers",
            "language": {
                "paginate": {
                    "next": "»",
                    "previous": "«",
                    "first": "First",
                    "last": "Last"
                }
            },
            "order": [[0, 'desc']],
            "columnDefs": [
                {
                    "targets": 0,
                    "type": "date",
                    "render": function(data, type, row) {
                        if (type === 'sort' || type === 'type') {
                            return convertToSortableDate(data);
                        }
                        return data;
                    }
                },
                {
                    "targets": 1,
                    "width": "60px",
                    "className": "time-column"
                }
            ],
            "drawCallback": function() {
                setupCancelConfirmation(type);
            }
        });

        console.log(`[FRIDA Enhancer] ✓ Kartu ${type} enhanced successfully!`);
    }

    function convertToSortableDate(dateStr) {
        const months = {
            'Januari': '01', 'Februari': '02', 'Maret': '03', 'April': '04',
            'Mei': '05', 'Juni': '06', 'Juli': '07', 'Agustus': '08',
            'September': '09', 'Oktober': '10', 'November': '11', 'Desember': '12'
        };

        const match = dateStr.match(/(\d+)\s+(\w+)\s+(\d{4})/);
        if (match) {
            const day = match[1].padStart(2, '0');
            const month = months[match[2]] || '01';
            const year = match[3];
            return `${year}-${month}-${day}`;
        }
        return dateStr;
    }

    function setupCancelConfirmation(type) {
        const eventType = type.toLowerCase();

        jQuery('form[action*="/cancel/"] button[type="submit"]').off('click').on('click', function(e) {
            e.preventDefault();
            const $form = jQuery(this).closest('form');

            if (typeof Swal !== 'undefined') {
                Swal.fire({
                    title: 'Konfirmasi',
                    text: `Yakin ingin membatalkan ${eventType} ini?`,
                    icon: 'warning',
                    showCancelButton: true,
                    confirmButtonColor: '#d33',
                    cancelButtonColor: '#6c757d',
                    confirmButtonText: 'Ya, Batalkan',
                    cancelButtonText: 'Tidak',
                    reverseButtons: true
                }).then((result) => {
                    if (result.isConfirmed) {
                        $form.submit();
                    }
                });
            } else {
                if (confirm(`Yakin ingin membatalkan ${eventType} ini?`)) {
                    $form.submit();
                }
            }
        });
    }

    // ============================================
    // JADWAL HANDLER WITH PROGRESSIVE LOADING
    // ============================================
    function handleJadwal(pageType, endpoint) {
        console.log(`[FRIDA Enhancer] ${pageType} page detected`);

        const waitForReady = setInterval(() => {
            if (typeof jQuery !== 'undefined' && typeof jQuery.fn.DataTable !== 'undefined') {
                clearInterval(waitForReady);

                setTimeout(() => {
                    startProgressiveLoading(pageType, endpoint);
                }, 1000);
            }
        }, 100);
    }

    async function startProgressiveLoading(pageType, endpoint) {
        const csrfToken = jQuery('input[name="_token"]').first().val();

        currentEndpoint = endpoint;
        currentCsrfToken = csrfToken;

        try {
            showLoadingOverlay();

            console.log(`[FRIDA Enhancer] Getting total records count...`);

            const initialResponse = await jQuery.ajax({
                url: `https://fridasv.com/mahasiswa/${endpoint}/server-side`,
                type: 'POST',
                headers: { 'X-CSRF-TOKEN': csrfToken },
                data: buildDataTablesParams(0, 1)
            });

            totalRecords = initialResponse.recordsTotal || 0;
            console.log(`[FRIDA Enhancer] Total records: ${totalRecords}`);

            await loadChunk(0, endpoint, csrfToken);

            initializeTableWithData(pageType, endpoint, csrfToken);

            setTimeout(() => {
                injectFilterUI();
                setupFilterHandlers();
            }, 1000);

            setupPaginationListener();

            hideLoadingOverlay();

        } catch (error) {
            console.error('[FRIDA Enhancer] Failed to load data:', error);
            hideLoadingOverlay();
            alert('Gagal memuat data. Silakan refresh halaman.');
        }
    }

    async function loadChunk(startIndex, endpoint, csrfToken) {
        const chunkNum = Math.floor(startIndex / CHUNK_SIZE) + 1;
        const totalChunks = Math.ceil(totalRecords / CHUNK_SIZE);

        updateLoadingProgress(`Memuat data... (${chunkNum}/${totalChunks})`, (chunkNum / totalChunks) * 100);

        console.log(`[FRIDA Enhancer] Loading chunk ${chunkNum}/${totalChunks} (${startIndex} - ${startIndex + CHUNK_SIZE})`);

        const response = await jQuery.ajax({
            url: `https://fridasv.com/mahasiswa/${endpoint}/server-side`,
            type: 'POST',
            headers: { 'X-CSRF-TOKEN': csrfToken },
            data: buildDataTablesParams(startIndex, CHUNK_SIZE)
        });

        const newData = response.data || [];
        allDataCache = allDataCache.concat(newData);

        console.log(`[FRIDA Enhancer] ✓ Loaded ${newData.length} records. Total in cache: ${allDataCache.length}`);

        return newData;
    }

    function buildDataTablesParams(start, length) {
        return {
            draw: 1,
            start: start,
            length: length,
            search: { value: '', regex: false },
            order: [{ column: 0, dir: 'asc' }],
            columns: [
                { data: 'no', searchable: true, orderable: true },
                { data: 'kehadiran', searchable: true, orderable: false },
                { data: 'tanggal', searchable: true, orderable: true },
                { data: 'waktu', searchable: true, orderable: true },
                { data: 'ruangan', searchable: true, orderable: true },
                { data: 'nama_pemrasaran', searchable: true, orderable: true },
                { data: 'nim_pemrasaran', searchable: true, orderable: true },
                { data: 'prodi', searchable: true, orderable: true },
                { data: 'judul', searchable: true, orderable: true },
                { data: 'jumlah_forum', searchable: true, orderable: false },
                { data: 'dosen_pembimbing', searchable: true, orderable: true },
                { data: 'moderator', searchable: true, orderable: true }
            ]
        };
    }

    function setupPaginationListener() {
        jQuery('#datatable1').on('page.dt', async function() {
            await checkAndLoadMore();
        });

        jQuery('#datatable1').on('length.dt', async function() {
            await checkAndLoadMore();
        });
    }

    async function checkAndLoadMore() {
        if (isLoadingMore || !dataTableInstance) return;
        if (allDataCache.length >= totalRecords) return;

        const info = dataTableInstance.page.info();
        const currentPage = info.page;
        const pageSize = info.length;

        const neededEnd = (currentPage + 1) * pageSize;
        const bufferEnd = neededEnd + PRELOAD_THRESHOLD;

        if (bufferEnd > allDataCache.length && allDataCache.length < totalRecords) {
            isLoadingMore = true;

            console.log(`[FRIDA Enhancer] Preloading next chunk... (current: ${allDataCache.length}, needed: ${bufferEnd})`);

            showMiniLoading();

            try {
                await loadChunk(allDataCache.length, currentEndpoint, currentCsrfToken);

                dataTableInstance.clear();
                dataTableInstance.rows.add(allDataCache);
                dataTableInstance.draw(false);

                populateProdiDropdown();
                updateDataTableInfo();

            } catch (error) {
                console.error('[FRIDA Enhancer] Failed to load more data:', error);
            } finally {
                hideMiniLoading();
                isLoadingMore = false;
            }
        }
    }

    function initializeTableWithData(pageType, endpoint, csrfToken) {
        const $table = jQuery('#datatable1');

        if ($table.length === 0) {
            console.error('[FRIDA Enhancer] Table not found!');
            return;
        }

        console.log(`[FRIDA Enhancer] Initializing ${pageType} table with ${allDataCache.length} records...`);

        if (jQuery.fn.DataTable.isDataTable('#datatable1')) {
            $table.DataTable().destroy();
        }

        dataTableInstance = $table.DataTable({
            "data": allDataCache,
            "responsive": true,
            "lengthChange": true,
            "aLengthMenu": [
                [10, 25, 50, 75, -1],
                [10, 25, 50, 75, "All"]
            ],
            "pageLength": 10,
            "autoWidth": false,
            "pagingType": "full_numbers",
            "scrollX": false,
            "language": {
                "paginate": {
                    "next": "&raquo;",
                    "previous": "&laquo;",
                    "first": "First",
                    "last": "Last"
                },
                "info": "Showing _START_ to _END_ of _TOTAL_ entries", // Will be customized
                "infoFiltered": "(filtered from _MAX_ entries)"
            },
            "columns": [
                { "data": "no" },
                { "data": "kehadiran" },
                { "data": "tanggal" },
                { "data": "waktu" },
                { "data": "ruangan" },
                { "data": "nama_pemrasaran" },
                { "data": "nim_pemrasaran" },
                { "data": "prodi" },
                { "data": "judul" },
                { "data": "jumlah_forum" },
                { "data": "dosen_pembimbing" },
                { "data": "moderator" }
            ],
            "columnDefs": [
                {
                    "targets": 1,
                    "orderable": false,
                    "render": function(data, type, row) {
                        const tgl = row.tanggal + ' ' + row.waktu;
                        const date = new Date(tgl);
                        const now = new Date();
                        const past = date < now;
                        const eventType = pageType.toLowerCase();

                        if (past) {
                            return `<a type="button" onclick="showAlert('pendaftaran ${eventType} sudah ditutup')"><span class="badge badge-pill button badge-secondary" style="font-size: 12px;"><i class="fas fa fa-check"></i>&ensp;Hadir</span></a>`;
                        } else if (row.nim_pemrasaran === "J0403221122") {
                            return `<a type="button" onclick="showAlert('tidak bisa menghadiri ${eventType} diri sendiri')"><span class="badge badge-pill button badge-secondary" style="font-size: 12px;"><i class="fas fa fa-check"></i>&ensp;Hadir</span></a>`;
                        } else if (row.set_bap_m === 1 || row.set_bap_d === 1 || row.set_bap_d_2 === 1) {
                            return `<a type="button" onclick="showAlert('BAP ${eventType} sudah diisi')"><span class="badge badge-pill button badge-secondary" style="font-size: 12px;"><i class="fas fa fa-check"></i>&ensp;Hadir</span></a>`;
                        } else {
                            return `<form id="submitKehadiran${row.kehadiran}" action="${endpoint}/hadir/${row.kehadiran}" method="POST" enctype="multipart/form-data">` +
                                    `<input type="hidden" name="_token" value="${csrfToken}">` +
                                    `<a href="javascript:void(0);" onclick="showConfirm('Yakin ingin menghadiri ${eventType} ini?', 'submitKehadiran${row.kehadiran}')"><span class="badge badge-pill button badge-success" style="font-size: 12px;"><i class="fas fa fa-check"></i>&ensp;Hadir</span></a>` +
                                    `</form>`;
                        }
                    }
                },
                {
                    "targets": 2,
                    "render": function(data, type, row) {
                        return new Date(data).toLocaleDateString('id-ID', {
                            weekday: 'long',
                            day: 'numeric',
                            month: 'long',
                            year: 'numeric'
                        });
                    }
                },
                {
                    "targets": 3,
                    "width": "60px",
                    "className": "time-column",
                    "render": function(data, type, row) {
                        if (!data) return '';
                        const timeParts = data.split(':');
                        if (timeParts.length >= 2) {
                            const hours = timeParts[0].padStart(2, '0');
                            const minutes = timeParts[1].padStart(2, '0');
                            return `${hours}:${minutes}`;
                        }
                        return data;
                    }
                },
                {
                    "targets": 4,
                    "render": function(data, type, row) {
                        if (!data) return '';
                        const isZoomLink = data.includes('zoom.us') ||
                                         data.includes('meet.google.com') ||
                                         (data.includes('http://') || data.includes('https://'));

                        if (isZoomLink) {
                            return `
                                <div class="room-cell zoom-room">
                                    <i class="fas fa-video zoom-icon"></i>
                                    <button class="btn-action btn-copy zoom-copy-btn" data-link="${data}" title="Copy Zoom Link">
                                        <i class="fas fa-copy"></i>
                                    </button>
                                    <button class="btn-action btn-open zoom-open-btn" data-link="${data}" title="Buka Zoom">
                                        <i class="fas fa-external-link-alt"></i>
                                    </button>
                                </div>
                            `;
                        } else {
                            return `
                                <div class="room-cell physical-room">
                                    <i class="fas fa-door-open"></i>
                                    <span>${data}</span>
                                </div>
                            `;
                        }
                    }
                },
                {
                    "targets": 7,
                    "createdCell": function(td, cellData, rowData, row, col) {
                        jQuery(td).addClass('text-container');
                    },
                    "render": function(data, type, row) {
                        return '<span class="nama-judul ellipsis">' + data + '</span><i class="fas fa-sort toggle-btn"></i>';
                    }
                },
                {
                    "targets": 8,
                    "createdCell": function(td, cellData, rowData, row, col) {
                        jQuery(td).addClass('text-container');
                    },
                    "render": function(data, type, row) {
                        return '<span class="nama-judul ellipsis">' + data + '</span><i class="fas fa-sort toggle-btn"></i>';
                    }
                },
                {
                    "targets": 9,
                    "orderable": false,
                    "createdCell": function(td, cellData, rowData, row, col) {
                        jQuery(td).css('text-align', 'center');
                    }
                }
            ],
            "drawCallback": function () {
                setupIconToggle();
                attachZoomHandlers();
                fixScrollbar();
                updateDataTableInfo();
            }
        });

        console.log(`[FRIDA Enhancer] ✓ ${pageType} table initialized!`);
    }

    // ============================================
    // UPDATE DATATABLE INFO TEXT
    // ============================================
    function updateDataTableInfo() {
        setTimeout(() => {
            const infoElement = document.querySelector('#datatable1_info');
            if (!infoElement) return;

            const info = dataTableInstance.page.info();
            const start = info.start + 1;
            const end = info.end;
            const displayed = info.recordsDisplay;
            const loaded = allDataCache.length;

            let infoText = `Showing ${start} to ${end} of ${displayed} entries`;

            // Add total count if not all data is loaded
            if (loaded < totalRecords) {
                infoText += ` <span style="color: #6c757d; font-weight: 600;">(${totalRecords.toLocaleString()} total)</span>`;
            }

            // If filtered
            if (displayed < loaded) {
                infoText = `Showing ${start} to ${end} of ${displayed} entries (filtered from ${loaded} loaded)`;
                if (loaded < totalRecords) {
                    infoText += ` <span style="color: #6c757d; font-weight: 600;">(${totalRecords.toLocaleString()} total)</span>`;
                }
            }

            infoElement.innerHTML = infoText;
        }, 50);
    }

    // ============================================
    // FILTER FUNCTIONS
    // ============================================
    function injectFilterUI() {
        const wrapper = document.querySelector('#datatable1_wrapper .row:first-child');
        if (!wrapper) return;

        const lengthDiv = wrapper.querySelector('.col-sm-12.col-md-6:first-child');
        const searchDiv = wrapper.querySelector('.col-sm-12.col-md-6:last-child');

        if (!lengthDiv || !searchDiv) return;

        lengthDiv.className = 'col-sm-12 col-md-3';
        searchDiv.className = 'col-sm-12 col-md-3';

        const filterDiv = document.createElement('div');
        filterDiv.className = 'col-sm-12 col-md-6';
        filterDiv.innerHTML = `
            <div class="dataTables_filter" id="frida-custom-filter">
                <label style="display: flex; align-items: center; gap: 6px; justify-content: center; margin-bottom: 0;">
                    <span style="font-size: 12px; font-weight: 600; white-space: nowrap;">Filter:</span>

                    <select id="filter-prodi" class="form-control form-control-sm" style="width: auto; min-width: 200px; max-width: 250px; font-size: 12px;">
                        <option value="">Semua Prodi</option>
                    </select>

                    <select id="filter-ruangan" class="form-control form-control-sm" style="width: auto; min-width: 90px; font-size: 12px;">
                        <option value="">Ruangan</option>
                        <option value="online">Online</option>
                        <option value="offline">Offline</option>
                    </select>

                    <select id="filter-status" class="form-control form-control-sm" style="width: auto; min-width: 100px; font-size: 12px;">
                        <option value="">Status</option>
                        <option value="available">Bisa Hadir</option>
                        <option value="unavailable">Tidak Bisa</option>
                    </select>

                    <button id="btn-clear-filter" class="btn btn-secondary btn-sm" style="font-size: 11px; padding: 5px 10px;" title="Reset Filter">
                        <i class="fas fa-redo"></i> Reset
                    </button>
                </label>
            </div>
        `;

        lengthDiv.insertAdjacentElement('afterend', filterDiv);

        populateProdiDropdown();

        console.log('[FRIDA Enhancer] Filter UI injected');
    }

    function populateProdiDropdown() {
        const prodiSet = new Set();

        allDataCache.forEach(row => {
            if (row.prodi) {
                prodiSet.add(row.prodi);
            }
        });

        const prodiSelect = jQuery('#filter-prodi');
        if (prodiSelect.length === 0) return;

        const currentValue = prodiSelect.val();
        prodiSelect.find('option:not([value=""])').remove();

        Array.from(prodiSet).sort().forEach(prodi => {
            prodiSelect.append(`<option value="${prodi}" title="${prodi}">${prodi}</option>`);
        });

        if (currentValue) {
            prodiSelect.val(currentValue);
        }

        console.log(`[FRIDA Enhancer] Populated ${prodiSet.size} prodi options`);
    }

    function setupFilterHandlers() {
        jQuery.fn.dataTable.ext.search.push(function(settings, data, dataIndex) {
            const row = allDataCache[dataIndex];
            if (!row) return true;

            const prodiFilter = jQuery('#filter-prodi').val();
            if (prodiFilter && prodiFilter !== '') {
                if (!row.prodi || !row.prodi.includes(prodiFilter)) {
                    return false;
                }
            }

            const ruanganFilter = jQuery('#filter-ruangan').val();
            if (ruanganFilter && ruanganFilter !== '') {
                const ruangan = row.ruangan || '';
                const isOnline = ruangan.includes('zoom.us') ||
                               ruangan.includes('meet.google.com') ||
                               ruangan.toLowerCase().includes('online') ||
                               ruangan.includes('http://') ||
                               ruangan.includes('https://');

                if (ruanganFilter === 'online' && !isOnline) return false;
                if (ruanganFilter === 'offline' && isOnline) return false;
            }

            const statusFilter = jQuery('#filter-status').val();
            if (statusFilter && statusFilter !== '') {
                const tgl = row.tanggal + ' ' + row.waktu;
                const date = new Date(tgl);
                const now = new Date();
                const past = date < now;

                const isSelf = row.nim_pemrasaran === "J0403221122";
                const bapFilled = row.set_bap_m === 1 || row.set_bap_d === 1 || row.set_bap_d_2 === 1;

                const isAvailable = !past && !isSelf && !bapFilled;

                if (statusFilter === 'available' && !isAvailable) return false;
                if (statusFilter === 'unavailable' && isAvailable) return false;
            }

            return true;
        });

        jQuery('#filter-prodi, #filter-ruangan, #filter-status').on('change', function() {
            if (dataTableInstance) {
                dataTableInstance.draw();
            }
        });

        jQuery('#btn-clear-filter').on('click', function() {
            jQuery('#filter-prodi').val('');
            jQuery('#filter-ruangan').val('');
            jQuery('#filter-status').val('');

            if (dataTableInstance) {
                dataTableInstance.draw();
                showSweetAlert('Filter direset!', 'info');
            }
        });
    }

    // ============================================
    // LOADING INDICATORS
    // ============================================
    function showLoadingOverlay() {
        const overlay = document.createElement('div');
        overlay.id = 'frida-loading-overlay';
        overlay.innerHTML = `
            <div style="text-align: center; color: white; max-width: 400px;">
                <i class="fas fa-spinner fa-spin" style="font-size: 48px; margin-bottom: 20px;"></i>
                <h3 id="loading-message">Memuat data...</h3>
                <div style="width: 100%; background: rgba(255,255,255,0.2); border-radius: 10px; overflow: hidden; height: 25px; margin-top: 15px;">
                    <div id="loading-progress-bar" style="width: 0%; height: 100%; background: linear-gradient(90deg, #28a745, #20c997); transition: width 0.3s ease;"></div>
                </div>
                <p id="loading-percentage" style="margin-top: 10px; font-size: 14px;">0%</p>
            </div>
        `;
        document.body.appendChild(overlay);
    }

    function updateLoadingProgress(message, percentage) {
        const messageEl = document.getElementById('loading-message');
        const progressBar = document.getElementById('loading-progress-bar');
        const percentageEl = document.getElementById('loading-percentage');

        if (messageEl) messageEl.textContent = message;
        if (progressBar) progressBar.style.width = percentage + '%';
        if (percentageEl) percentageEl.textContent = Math.round(percentage) + '%';
    }

    function hideLoadingOverlay() {
        const overlay = document.getElementById('frida-loading-overlay');
        if (overlay) {
            overlay.remove();
        }
    }

    function showMiniLoading() {
        const mini = document.createElement('div');
        mini.id = 'frida-mini-loading';
        mini.innerHTML = `
            <i class="fas fa-spinner fa-spin"></i> Memuat data tambahan...
        `;
        document.body.appendChild(mini);
    }

    function hideMiniLoading() {
        const mini = document.getElementById('frida-mini-loading');
        if (mini) {
            mini.remove();
        }
    }

    // ============================================
    // SHARED UTILITIES
    // ============================================
    function fixScrollbar() {
        setTimeout(() => {
            const wrapper = document.querySelector('.table-responsive');
            const table = document.querySelector('#datatable1');

            if (wrapper && table) {
                const wrapperWidth = wrapper.clientWidth;
                const tableWidth = table.scrollWidth;

                if (tableWidth <= wrapperWidth + 5) {
                    wrapper.style.overflowX = 'hidden';
                    wrapper.style.width = '100%';
                    table.style.width = '100%';
                    table.style.maxWidth = '100%';
                }
            }
        }, 100);
    }

    function attachZoomHandlers() {
        jQuery('.zoom-copy-btn').off('click').on('click', function(e) {
            e.preventDefault();
            e.stopPropagation();

            const link = jQuery(this).data('link');

            if (typeof GM_setClipboard !== 'undefined') {
                GM_setClipboard(link);
                showSweetAlert('Link Zoom berhasil disalin!', 'success');
            } else {
                if (navigator.clipboard) {
                    navigator.clipboard.writeText(link).then(() => {
                        showSweetAlert('Link Zoom berhasil disalin!', 'success');
                    }).catch(() => {
                        fallbackCopy(link);
                    });
                } else {
                    fallbackCopy(link);
                }
            }
        });

        jQuery('.zoom-open-btn').off('click').on('click', function(e) {
            e.preventDefault();
            e.stopPropagation();

            const link = jQuery(this).data('link');
            window.open(link, '_blank', 'noopener,noreferrer');
            showSweetAlert('Membuka Zoom di tab baru...', 'info');
        });
    }

    function fallbackCopy(text) {
        const textarea = document.createElement('textarea');
        textarea.value = text;
        textarea.style.position = 'fixed';
        textarea.style.opacity = '0';
        document.body.appendChild(textarea);
        textarea.select();
        try {
            document.execCommand('copy');
            showSweetAlert('Link Zoom berhasil disalin!', 'success');
        } catch (err) {
            showSweetAlert('Gagal menyalin link', 'error');
        }
        document.body.removeChild(textarea);
    }

    function showSweetAlert(message, icon) {
        if (typeof Swal !== 'undefined') {
            const Toast = Swal.mixin({
                toast: true,
                position: 'top-end',
                showConfirmButton: false,
                timer: 3000,
                timerProgressBar: true,
                didOpen: (toast) => {
                    toast.addEventListener('mouseenter', Swal.stopTimer);
                    toast.addEventListener('mouseleave', Swal.resumeTimer);
                }
            });

            Toast.fire({
                icon: icon,
                title: message,
                customClass: {
                    popup: 'colored-toast'
                }
            });
        }
    }

    function setupIconToggle() {
        document.querySelectorAll('.text-container').forEach(container => {
            const textElement = container.querySelector('.ellipsis');
            const icon = container.querySelector('.toggle-btn');

            if (!textElement || !icon) return;

            if (textElement.scrollWidth > textElement.clientWidth) {
                icon.style.display = 'inline';
            }

            const newIcon = icon.cloneNode(true);
            icon.parentNode.replaceChild(newIcon, icon);

            newIcon.addEventListener('click', () => {
                if (textElement.classList.contains('ellipsis')) {
                    textElement.classList.remove('ellipsis');
                    textElement.classList.add('full-text');
                } else {
                    textElement.classList.remove('full-text');
                    textElement.classList.add('ellipsis');
                }
            });
        });
    }

    function injectCustomCSS() {
        const style = document.createElement('style');
        style.id = 'frida-enhancer-styles';
        style.textContent = `
            .card-body { overflow: visible !important; }
            #datatable1_wrapper { width: 100% !important; overflow: visible !important; }
            #datatable1 { width: 100% !important; max-width: 100% !important; table-layout: auto !important; }

            .table-responsive {
                overflow-x: hidden !important;
                width: 100% !important;
                -webkit-overflow-scrolling: touch !important;
            }

            #frida-loading-overlay {
                position: fixed;
                top: 0;
                left: 0;
                width: 100%;
                height: 100%;
                background: rgba(0, 0, 0, 0.85);
                display: flex;
                align-items: center;
                justify-content: center;
                z-index: 99999;
            }

            #frida-mini-loading {
                position: fixed;
                bottom: 20px;
                right: 20px;
                background: rgba(0, 0, 0, 0.8);
                color: white;
                padding: 15px 20px;
                border-radius: 8px;
                z-index: 9999;
                font-size: 14px;
                box-shadow: 0 4px 12px rgba(0,0,0,0.3);
            }

            td.time-column,
            th.time-column {
                width: 60px !important;
                max-width: 60px !important;
                min-width: 60px !important;
                text-align: center !important;
                padding-left: 8px !important;
                padding-right: 8px !important;
                white-space: nowrap !important;
            }

            #frida-custom-filter label {
                margin-bottom: 0 !important;
            }

            #filter-prodi {
                min-width: 200px !important;
                max-width: 250px !important;
            }

            .room-cell {
                display: flex;
                align-items: center;
                gap: 6px;
                font-size: 12px;
                white-space: nowrap;
            }

            .zoom-room { flex-wrap: nowrap; }
            .zoom-icon { color: #2D8CFF; font-size: 14px; margin-right: 2px; flex-shrink: 0; }

            .physical-room { color: #28a745; font-weight: 600; }
            .physical-room i { color: #28a745; font-size: 13px; flex-shrink: 0; }
            .physical-room span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 100px; }

            .btn-action {
                padding: 4px 8px;
                font-size: 11px;
                border: none;
                border-radius: 4px;
                cursor: pointer;
                transition: all 0.2s ease;
                background: white;
                color: #555;
                border: 1px solid #ddd;
                flex-shrink: 0;
            }

            .btn-action:hover { transform: translateY(-1px); box-shadow: 0 2px 4px rgba(0,0,0,0.15); }
            .btn-action i { font-size: 10px; }
            .btn-copy:hover { background: #e3f2fd; border-color: #2D8CFF; color: #2D8CFF; }
            .btn-open:hover { background: #2D8CFF; border-color: #2D8CFF; color: white; }

            .full-text { white-space: normal !important; overflow: visible !important; max-width: none !important; }

            .colored-toast.swal2-icon-success { background-color: #28a745 !important; }
            .colored-toast.swal2-icon-error { background-color: #dc3545 !important; }
            .colored-toast.swal2-icon-info { background-color: #17a2b8 !important; }

            @media (max-width: 768px) {
                .room-cell { flex-wrap: wrap; }
                .card-body { padding: 0.5rem !important; }
                .table-responsive { overflow-x: auto !important; }

                td.time-column,
                th.time-column {
                    width: 50px !important;
                    max-width: 50px !important;
                    min-width: 50px !important;
                    font-size: 11px !important;
                }

                #filter-prodi {
                    min-width: 150px !important;
                    max-width: 200px !important;
                }
            }
        `;
        document.head.appendChild(style);
    }

})();