diff --git a/coretabs.user.js b/coretabs.user.js index bd9b6b0..dec19c9 100644 --- a/coretabs.user.js +++ b/coretabs.user.js @@ -10,25 +10,53 @@ // @run-at document-idle // ==/UserScript== -(function() { - 'use strict'; +(function () { + "use strict"; - // --- SCRIPT CONFIGURATION --- - const AUTH_STORAGE_KEY = 'cats-angular-clientuser:https://coretax.intranet.pajak.go.id/identityprovider:cats-angular-client'; - const DEFAULT_CASES_FILTER = 'In Progress'; - const REFUND_CASE_PREFIX = 'Pengembalian'; - // ---------------------------- + // Prevent running inside iframes and prevent duplicate injection into the top window. + // This ensures only one instance of the sidebar is created even if the userscript is executed + // for multiple frames on the same page. + try { + if (window.top !== window.self) { + // We're in an iframe - do nothing. + return; + } + } catch (e) { + // In rare cross-origin cases accessing window.top may throw; if so, abort to be safe. + return; + } - // --- State Management --- - let allMyCases = [], allCaseDocuments = [], allCaseUsers = [], refundReviewData = []; - let filteredRefundData = []; - let selectedCaseId = null; - let loadedDocsForCaseId = null; - let loadedUsersForCaseId = null; + // Guard against double-injection in the top window (e.g., userscript re-run) + if (window.__coretabs_injected) { + return; + } + Object.defineProperty(window, "__coretabs_injected", { + value: true, + configurable: false, + writable: false, + enumerable: false, + }); - function addStyles() { - GM_addStyle(` - #ct-sidebar{position:fixed;top:100px;right:-950px;width:950px;height:80vh;max-height:800px;background-color:#f9f9f9;border:1px solid #ccc;border-radius:8px 0 0 8px;box-shadow:-3px 0 8px rgba(0,0,0,.15);z-index:9999;transition:right .4s ease-in-out;display:flex;flex-direction:column;font-family:sans-serif}#ct-sidebar.open{right:0}#ct-sidebar-toggle{position:absolute;top:50%;right:950px;transform:translateY(-50%);width:30px;height:80px;background-color:#0056b3;color:#fff;border:none;border-radius:8px 0 0 8px;cursor:pointer;writing-mode:vertical-rl;text-orientation:mixed;display:flex;align-items:center;justify-content:center;font-size:14px;font-weight:700;letter-spacing:1px} + // --- SCRIPT CONFIGURATION --- + const AUTH_STORAGE_KEY = + "cats-angular-clientuser:https://coretax.intranet.pajak.go.id/identityprovider:cats-angular-client"; + const DEFAULT_CASES_FILTER = "In Progress"; + const REFUND_CASE_PREFIX = "Pengembalian"; + // ---------------------------- + + // --- State Management --- + let allMyCases = [], + allCaseDocuments = [], + allCaseUsers = [], + refundReviewData = []; + let filteredRefundData = []; + let selectedCaseId = null; + let loadedDocsForCaseId = null; + let loadedUsersForCaseId = null; + + function addStyles() { + GM_addStyle(` + #ct-sidebar{position:fixed;top:100px;right:-950px;width:950px;height:80vh;max-height:800px;background-color:#f9f9f9;border:1px solid #ccc;border-radius:8px 0 0 8px;box-shadow:-3px 0 8px rgba(0,0,0,.15);z-index:2147483647;transition:right .4s ease-in-out;display:flex;flex-direction:column;font-family:sans-serif}#ct-sidebar.open{right:0}#ct-sidebar-toggle{position:absolute;top:50%;right:950px;transform:translateY(-50%);width:30px;height:80px;background-color:#0056b3;color:#fff;border:none;border-radius:8px 0 0 8px;cursor:pointer;writing-mode:vertical-rl;text-orientation:mixed;display:flex;align-items:center;justify-content:center;font-size:14px;font-weight:700;letter-spacing:1px} #ct-header-area { padding: 12px 15px; background-color: #343a40; color: white; flex-shrink: 0; display: flex; align-items: center; gap: 15px; border-bottom: 1px solid #495057; } #ct-header-icon { flex-shrink: 0; } #ct-header-icon svg { width: 32px; height: 32px; fill: #e9ecef; } @@ -56,13 +84,16 @@ .currency-wrapper { display: flex; justify-content: space-between; } .currency-num { font-variant-numeric: tabular-nums; } `); - } + } - function createSidebar() { - const sidebarContainer = document.createElement('div'); - sidebarContainer.innerHTML = ` + function createSidebar() { + // ensure we don't create duplicate DOM elements even if something else tries to run + if (document.getElementById("ct-sidebar")) return; + + const sidebarContainer = document.createElement("div"); + sidebarContainer.innerHTML = `
`) + : (errorHtml = `An error occurred:
${error.message}
Step 1/3: Fetching Sub Process ID...
`; + if (!caseId) throw new Error("A case must be selected."); + const subProcessId = await fetchSubProcessId(caseId); + responseArea.innerHTML = `Step 2/3: Fetching reference number (trying primary API)...
`; + const referenceNumber = await fetchC02FormDetail(caseId, subProcessId); + responseArea.innerHTML = `Step 3/3: Fetching refund details...
`; + await fetchRefundReview(caseId, referenceNumber); + } catch (error) { + handleError(error, responseArea); + } finally { + button.textContent = originalText; + button.disabled = false; + } + } + + function handleCaseSelection(event) { + const selectedRow = event.target.closest("tr"); + if (!selectedRow || selectedRow.classList.contains("group-header")) return; + const caseId = selectedRow.dataset.id; + if (!caseId || caseId === selectedCaseId) return; + + const selectedCase = allMyCases.find( + (e) => e.AggregateIdentifier === caseId, + ); + updateHeader(selectedCase); + selectedCaseId = caseId; + loadedDocsForCaseId = null; + loadedUsersForCaseId = null; + refundReviewData = []; + filteredRefundData = []; + document.querySelector("#tab-refund .results-container").innerHTML = + `Please use the 'Review Refund' action on a relevant case.
`; + document.getElementById("refund-download-btn").disabled = true; + + const allRows = selectedRow.closest("tbody").querySelectorAll("tr"); + allRows.forEach((e) => e.classList.remove("selected")); + selectedRow.classList.add("selected"); + + const actionButton = event.target.closest(".action-btn"); + if (actionButton) { + if (actionButton.matches(".view-docs")) { + switchTab("tab-docs"); + } else if (actionButton.matches(".view-users")) { + switchTab("tab-users"); + } else if (actionButton.matches(".review-refund-case")) { + startRefundReviewProcess(caseId, actionButton); + } + } + } + function handleDocumentAction(event) { + const target = event.target.closest(".action-btn"); + if (!target) return; + if (target.matches(".download-doc")) { + const docId = target.dataset.docId; + const filename = target.dataset.filename; + downloadDocument(docId, filename, target); + } + } + function handleGroupToggle(event) { + const headerRow = event.target.closest(".group-header"); + if (!headerRow) return; + const groupId = headerRow.dataset.groupId; + if (!groupId) return; + headerRow.classList.toggle("collapsed"); + headerRow.classList.toggle("expanded"); + const isCollapsed = headerRow.classList.contains("collapsed"); + const tableBody = headerRow.parentElement; + const memberRows = tableBody.querySelectorAll(`.group-member.${groupId}`); + memberRows.forEach((row) => { + row.style.display = isCollapsed ? "none" : "table-row"; + }); + } + function switchTab(tabId) { + (document + .querySelectorAll(".ct-tab-button") + .forEach((e) => e.classList.remove("active")), + document + .querySelectorAll(".ct-tab-panel") + .forEach((e) => e.classList.remove("active")), + document.querySelector(`[data-tab="${tabId}"]`).classList.add("active"), + document.getElementById(tabId).classList.add("active"), + "tab-docs" === tabId + ? selectedCaseId && + selectedCaseId !== loadedDocsForCaseId && + fetchCaseDocuments(selectedCaseId) + : "tab-users" === tabId && + selectedCaseId && + selectedCaseId !== loadedUsersForCaseId && + fetchCaseUsers(selectedCaseId)); + } + function downloadRefundExcel() { + if (!filteredRefundData || filteredRefundData.length === 0) { + alert("No data to download. Please filter the data first."); + return; + } + const now = new Date(); + const year = now.getFullYear(); + const month = String(now.getMonth() + 1).padStart(2, "0"); + const day = String(now.getDate()).padStart(2, "0"); + const hours = String(now.getHours()).padStart(2, "0"); + const minutes = String(now.getMinutes()).padStart(2, "0"); + const datetimeSuffix = `${year}${month}${day}_${hours}${minutes}`; + let filenamePrefix = "Refund_Review_Data"; + if (selectedCaseId) { + const selectedCase = allMyCases.find( + (c) => c.AggregateIdentifier === selectedCaseId, + ); + if (selectedCase && selectedCase.CaseNumber) { + const sanitizedCaseNumber = selectedCase.CaseNumber.replace( + /[\\/:"*?<>|]/g, + "_", + ); + filenamePrefix = `Refund_Review_Data_${sanitizedCaseNumber}`; + } + } + const filename = `${filenamePrefix}_${datetimeSuffix}.xlsx`; + const headerMapping = [ + { + apiField: "ReportedBySeller", + excelHeader: "Telah dilaporkan oleh Penjual?", + }, + { apiField: "Tin", excelHeader: "NPWP Penjual" }, + { apiField: "Name", excelHeader: "Nama Penjual" }, + { + apiField: "DocumentNumber", + excelHeader: + "Nomor Faktur Pajak/Dokumen yang Dipersamakan/Nota Retur/Nota Pembatalan", + }, + { + apiField: "DocumentDate", + excelHeader: + "Tanggal Faktur Pajak/Dokumen yang Dipersamakan/Nota Retur/Nota Pembatalan", + }, + { apiField: "TransactionCode", excelHeader: "Kode Transaksi" }, + { + apiField: "SellingPrice", + excelHeader: + "Harga Jual/Dasar Pengenaan Pajak/Dasar Pengenaan Pajak Lainnya (Rp)", + }, + { + apiField: "VatPaid", + excelHeader: "PPN yang dikreditkan pada SPT yang Dilaporkan", + }, + { + apiField: "StlgPaid", + excelHeader: "PPnBM yang dikreditkan pada SPT yang Dilaporkan", + }, + ]; + const excelData = filteredRefundData.map((item) => { + const row = {}; + headerMapping.forEach((map) => { + if (map.apiField === "ReportedBySeller") { + row[map.excelHeader] = item[map.apiField] ? "Yes" : "No"; } else { - titleEl.textContent = 'No Case Selected'; - subtitleEl.textContent = 'Please select a case from the "My Cases" tab'; - } - } - - // MODIFIED: This function has updated user-facing messages. - async function startRefundReviewProcess(caseId, button) { - const originalText = button.textContent; - button.textContent = '...'; - button.disabled = true; - const responseArea = document.querySelector('#tab-refund .results-container'); - try { - switchTab('tab-refund'); - responseArea.innerHTML = `Step 1/3: Fetching Sub Process ID...
`; - if (!caseId) throw new Error("A case must be selected."); - const subProcessId = await fetchSubProcessId(caseId); - responseArea.innerHTML = `Step 2/3: Fetching reference number (trying primary API)...
`; - const referenceNumber = await fetchC02FormDetail(caseId, subProcessId); - responseArea.innerHTML = `Step 3/3: Fetching refund details...
`; - await fetchRefundReview(caseId, referenceNumber); - } catch(error) { - handleError(error, responseArea); - } finally { - button.textContent = originalText; - button.disabled = false; + row[map.excelHeader] = + item[map.apiField] === null ? "" : item[map.apiField]; } + }); + return row; + }); + const worksheet = XLSX.utils.json_to_sheet(excelData); + const workbook = XLSX.utils.book_new(); + XLSX.utils.book_append_sheet(workbook, worksheet, "Refund Review"); + XLSX.writeFile(workbook, filename); + } + + function toggleAllGroups(containerSelector, button) { + const isCollapsing = button.textContent === "Collapse All"; + const headers = document.querySelectorAll( + `${containerSelector} .group-header`, + ); + const members = document.querySelectorAll( + `${containerSelector} .group-member`, + ); + + headers.forEach((h) => { + h.classList.toggle("expanded", !isCollapsing); + h.classList.toggle("collapsed", isCollapsing); + }); + members.forEach((m) => { + m.style.display = isCollapsing ? "none" : "table-row"; + }); + + button.textContent = isCollapsing ? "Expand All" : "Collapse All"; + } + + // --- MAIN INITIALIZATION --- + function main() { + addStyles(); + createSidebar(); + const toggleBtn = document.getElementById("ct-sidebar-toggle"); + if (toggleBtn) { + toggleBtn.addEventListener("click", () => { + const sidebar = document.getElementById("ct-sidebar"); + if (sidebar) sidebar.classList.toggle("open"); + }); } - function handleCaseSelection(event) { - const selectedRow = event.target.closest("tr"); - if (!selectedRow || selectedRow.classList.contains("group-header")) return; - const caseId = selectedRow.dataset.id; - if (!caseId || caseId === selectedCaseId) return; - - const selectedCase = allMyCases.find(e => e.AggregateIdentifier === caseId); - updateHeader(selectedCase); - selectedCaseId = caseId; - loadedDocsForCaseId = null; - loadedUsersForCaseId = null; - refundReviewData = []; - filteredRefundData = []; - document.querySelector('#tab-refund .results-container').innerHTML = `Please use the 'Review Refund' action on a relevant case.
`; - document.getElementById('refund-download-btn').disabled = true; - - const allRows = selectedRow.closest("tbody").querySelectorAll("tr"); - allRows.forEach(e => e.classList.remove("selected")); - selectedRow.classList.add("selected"); - - const actionButton = event.target.closest(".action-btn"); - if (actionButton) { - if (actionButton.matches(".view-docs")) { switchTab("tab-docs"); } - else if (actionButton.matches(".view-users")) { switchTab("tab-users"); } - else if (actionButton.matches(".review-refund-case")) { startRefundReviewProcess(caseId, actionButton); } - } - } - function handleDocumentAction(event) { - const target = event.target.closest('.action-btn'); - if (!target) return; - if (target.matches('.download-doc')) { - const docId = target.dataset.docId; - const filename = target.dataset.filename; - downloadDocument(docId, filename, target); - } - } - function handleGroupToggle(event) { - const headerRow = event.target.closest('.group-header'); - if (!headerRow) return; - const groupId = headerRow.dataset.groupId; - if (!groupId) return; - headerRow.classList.toggle('collapsed'); - headerRow.classList.toggle('expanded'); - const isCollapsed = headerRow.classList.contains('collapsed'); - const tableBody = headerRow.parentElement; - const memberRows = tableBody.querySelectorAll(`.group-member.${groupId}`); - memberRows.forEach(row => { row.style.display = isCollapsed ? 'none' : 'table-row'; }); - } - function switchTab(tabId){document.querySelectorAll(".ct-tab-button").forEach(e=>e.classList.remove("active")),document.querySelectorAll(".ct-tab-panel").forEach(e=>e.classList.remove("active")),document.querySelector(`[data-tab="${tabId}"]`).classList.add("active"),document.getElementById(tabId).classList.add("active"),"tab-docs"===tabId?selectedCaseId&&selectedCaseId!==loadedDocsForCaseId&&fetchCaseDocuments(selectedCaseId):"tab-users"===tabId&&selectedCaseId&&selectedCaseId!==loadedUsersForCaseId&&fetchCaseUsers(selectedCaseId)} - function downloadRefundExcel() { - if (!filteredRefundData || filteredRefundData.length === 0) { - alert("No data to download. Please filter the data first."); - return; - } - const now = new Date(); - const year = now.getFullYear(); - const month = String(now.getMonth() + 1).padStart(2, '0'); - const day = String(now.getDate()).padStart(2, '0'); - const hours = String(now.getHours()).padStart(2, '0'); - const minutes = String(now.getMinutes()).padStart(2, '0'); - const datetimeSuffix = `${year}${month}${day}_${hours}${minutes}`; - let filenamePrefix = 'Refund_Review_Data'; - if (selectedCaseId) { - const selectedCase = allMyCases.find(c => c.AggregateIdentifier === selectedCaseId); - if (selectedCase && selectedCase.CaseNumber) { - const sanitizedCaseNumber = selectedCase.CaseNumber.replace(/[\\/:"*?<>|]/g, '_'); - filenamePrefix = `Refund_Review_Data_${sanitizedCaseNumber}`; - } - } - const filename = `${filenamePrefix}_${datetimeSuffix}.xlsx`; - const headerMapping = [ - { apiField: "ReportedBySeller", excelHeader: "Telah dilaporkan oleh Penjual?" }, { apiField: "Tin", excelHeader: "NPWP Penjual" }, { apiField: "Name", excelHeader: "Nama Penjual" }, { apiField: "DocumentNumber", excelHeader: "Nomor Faktur Pajak/Dokumen yang Dipersamakan/Nota Retur/Nota Pembatalan" }, { apiField: "DocumentDate", excelHeader: "Tanggal Faktur Pajak/Dokumen yang Dipersamakan/Nota Retur/Nota Pembatalan" }, { apiField: "TransactionCode", excelHeader: "Kode Transaksi" }, { apiField: "SellingPrice", excelHeader: "Harga Jual/Dasar Pengenaan Pajak/Dasar Pengenaan Pajak Lainnya (Rp)" }, { apiField: "VatPaid", excelHeader: "PPN yang dikreditkan pada SPT yang Dilaporkan" }, { apiField: "StlgPaid", excelHeader: "PPnBM yang dikreditkan pada SPT yang Dilaporkan" } - ]; - const excelData = filteredRefundData.map(item => { - const row = {}; - headerMapping.forEach(map => { - if (map.apiField === 'ReportedBySeller') { - row[map.excelHeader] = item[map.apiField] ? 'Yes' : 'No'; - } else { - row[map.excelHeader] = item[map.apiField] === null ? '' : item[map.apiField]; - } - }); - return row; - }); - const worksheet = XLSX.utils.json_to_sheet(excelData); - const workbook = XLSX.utils.book_new(); - XLSX.utils.book_append_sheet(workbook, worksheet, "Refund Review"); - XLSX.writeFile(workbook, filename); + const tabBar = document.getElementById("ct-tab-bar"); + if (tabBar) { + tabBar.addEventListener("click", (e) => { + if (e.target.matches(".ct-tab-button")) switchTab(e.target.dataset.tab); + }); } - function toggleAllGroups(containerSelector, button) { - const isCollapsing = button.textContent === 'Collapse All'; - const headers = document.querySelectorAll(`${containerSelector} .group-header`); - const members = document.querySelectorAll(`${containerSelector} .group-member`); + const casesFilterEl = document.getElementById("cases-status-filter"); + if (casesFilterEl) + casesFilterEl.addEventListener("change", renderMyCasesTable); - headers.forEach(h => { - h.classList.toggle('expanded', !isCollapsing); - h.classList.toggle('collapsed', isCollapsing); - }); - members.forEach(m => { - m.style.display = isCollapsing ? 'none' : 'table-row'; - }); + const docsFilterEl = document.getElementById("docs-status-filter"); + if (docsFilterEl) + docsFilterEl.addEventListener("change", renderCaseDocumentsTable); - button.textContent = isCollapsing ? 'Expand All' : 'Collapse All'; - } + const usersFilterEl = document.getElementById("users-role-filter"); + if (usersFilterEl) + usersFilterEl.addEventListener("change", renderCaseUsersTable); - // --- MAIN INITIALIZATION --- - function main() { - addStyles(); - createSidebar(); - document.getElementById('ct-sidebar-toggle').addEventListener('click', () => { - document.getElementById('ct-sidebar').classList.toggle('open'); - }); - document.getElementById('ct-tab-bar').addEventListener('click', (e) => { - if (e.target.matches('.ct-tab-button')) switchTab(e.target.dataset.tab); - }); - document.getElementById('cases-status-filter').addEventListener('change', renderMyCasesTable); - document.getElementById('docs-status-filter').addEventListener('change', renderCaseDocumentsTable); - document.getElementById('users-role-filter').addEventListener('change', renderCaseUsersTable); - document.getElementById('refund-reported-filter').addEventListener('change', renderRefundReviewTable); - document.getElementById('refund-download-btn').addEventListener('click', downloadRefundExcel); + const refundFilterEl = document.getElementById("refund-reported-filter"); + if (refundFilterEl) + refundFilterEl.addEventListener("change", renderRefundReviewTable); - document.getElementById('toggle-cases-btn').addEventListener('click', (e) => toggleAllGroups('#tab-my-cases .results-container', e.target)); - document.getElementById('toggle-docs-btn').addEventListener('click', (e) => toggleAllGroups('#tab-docs .results-container', e.target)); - document.getElementById('toggle-refund-btn').addEventListener('click', (e) => toggleAllGroups('#tab-refund .results-container', e.target)); + const refundDownloadBtn = document.getElementById("refund-download-btn"); + if (refundDownloadBtn) + refundDownloadBtn.addEventListener("click", downloadRefundExcel); - fetchMyCases(); - } + const toggleCasesBtn = document.getElementById("toggle-cases-btn"); + if (toggleCasesBtn) + toggleCasesBtn.addEventListener("click", (e) => + toggleAllGroups("#tab-my-cases .results-container", e.target), + ); + const toggleDocsBtn = document.getElementById("toggle-docs-btn"); + if (toggleDocsBtn) + toggleDocsBtn.addEventListener("click", (e) => + toggleAllGroups("#tab-docs .results-container", e.target), + ); + + const toggleRefundBtn = document.getElementById("toggle-refund-btn"); + if (toggleRefundBtn) + toggleRefundBtn.addEventListener("click", (e) => + toggleAllGroups("#tab-refund .results-container", e.target), + ); + + // initial load + fetchMyCases(); + } + + // Run main when DOM is ready (script runs at document-idle but ensure body exists) + if (document.readyState === "loading") { + document.addEventListener("DOMContentLoaded", main); + } else { main(); -})(); \ No newline at end of file + } +})();