From b38ee62ef99af5e6ccb4131343e4e0d7c80617b0 Mon Sep 17 00:00:00 2001 From: Dias Baskara <25913324+diasbaskara@users.noreply.github.com> Date: Wed, 26 Nov 2025 10:47:44 +0700 Subject: [PATCH 1/3] Prevent sidebar injection in iframes and duplicate loads Added guards to ensure the userscript sidebar is only injected into the top window and not into iframes. Also prevents duplicate sidebar injection in the top window. Updated z-index for sidebar, improved DOM creation safety, and made minor UI text changes. --- coretabs.user.js | 1164 +++++++++++++++++++++++++++++++++++----------- 1 file changed, 889 insertions(+), 275 deletions(-) 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 + } +})(); -- 2.49.1 From b43fae30b1138f1055c292b2c6033a26ffc50b01 Mon Sep 17 00:00:00 2001 From: Dias Baskara <25913324+diasbaskara@users.noreply.github.com> Date: Wed, 26 Nov 2025 10:49:30 +0700 Subject: [PATCH 2/3] Add overlay to close sidebar on outside click Introduced a transparent overlay that appears when the sidebar is open, allowing users to close the sidebar by clicking outside of it. This improves usability by providing a more intuitive way to dismiss the sidebar. --- coretabs.user.js | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/coretabs.user.js b/coretabs.user.js index dec19c9..3c8065d 100644 --- a/coretabs.user.js +++ b/coretabs.user.js @@ -57,6 +57,7 @@ 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-overlay{position:fixed;top:0;left:0;width:100%;height:100%;background-color:transparent;z-index:2147483646;display:none} #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; } @@ -127,6 +128,11 @@ `; // Append to document.body (we are running only in top window so this is safe) document.body.appendChild(sidebarContainer); + + // Create overlay for click-outside functionality + const overlay = document.createElement("div"); + overlay.className = "ct-overlay"; + document.body.appendChild(overlay); } // --- RENDER FUNCTIONS --- @@ -944,7 +950,26 @@ if (toggleBtn) { toggleBtn.addEventListener("click", () => { const sidebar = document.getElementById("ct-sidebar"); - if (sidebar) sidebar.classList.toggle("open"); + const overlay = document.querySelector(".ct-overlay"); + if (sidebar) { + sidebar.classList.toggle("open"); + if (overlay) + overlay.style.display = sidebar.classList.contains("open") + ? "block" + : "none"; + } + }); + } + + // Add click outside handler + const overlay = document.querySelector(".ct-overlay"); + if (overlay) { + overlay.addEventListener("click", () => { + const sidebar = document.getElementById("ct-sidebar"); + if (sidebar && sidebar.classList.contains("open")) { + sidebar.classList.remove("open"); + overlay.style.display = "none"; + } }); } -- 2.49.1 From 50401a73c9647c1304a4e7d5a3725e3cfeb8dea1 Mon Sep 17 00:00:00 2001 From: Dias Baskara <25913324+diasbaskara@users.noreply.github.com> Date: Wed, 26 Nov 2025 13:02:55 +0700 Subject: [PATCH 3/3] Add Case History tab with grouped history view Introduces a new 'Case History' tab to display case routing history, grouped by case type and including current role information. Implements API calls to fetch history and subprocess data, adds filtering and group toggling, and updates tab switching logic to support the new feature. --- coretabs.user.js | 258 +++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 239 insertions(+), 19 deletions(-) diff --git a/coretabs.user.js b/coretabs.user.js index 3c8065d..4aecdbe 100644 --- a/coretabs.user.js +++ b/coretabs.user.js @@ -48,11 +48,14 @@ let allMyCases = [], allCaseDocuments = [], allCaseUsers = [], - refundReviewData = []; - let filteredRefundData = []; - let selectedCaseId = null; - let loadedDocsForCaseId = null; - let loadedUsersForCaseId = null; + caseHistoryData = [], + caseSubProcessData = {}, + refundReviewData = [], + filteredRefundData = [], + selectedCaseId = null, + loadedDocsForCaseId = null, + loadedUsersForCaseId = null, + loadedHistoryForCaseId = null; function addStyles() { GM_addStyle(` @@ -109,12 +112,20 @@ +Loading my cases...
Please select a case to view its documents.
Please select a case to view its users.
Please select a case to view its history.
No history data available for this case.
'; + return; + } + + const filterValue = document.getElementById("history-type-filter").value; + const filteredHistory = caseHistoryData.filter( + (item) => filterValue === "all" || item.CaseType === filterValue, + ); + + if (filteredHistory.length === 0) { + responseArea.innerHTML = + 'No history items match the selected filter.
'; + return; + } + + const table = createTable([ + "Routing Date", + "Performed By", + "Workflow Step", + ]), + tbody = document.createElement("tbody"); + + // Group items by CaseType + const groupedHistory = {}; + filteredHistory.forEach((item) => { + const caseType = item.CaseType || "Unknown"; + if (!groupedHistory[caseType]) { + groupedHistory[caseType] = []; + } + groupedHistory[caseType].push(item); + }); + + // Create table rows with grouping + Object.keys(groupedHistory).forEach((caseType) => { + const groupItems = groupedHistory[caseType]; + + // Create group header row + const sanitizedCaseType = caseType.replace(/[^a-zA-Z0-9]/g, "_"); + tbody.innerHTML += `Loading history...
'; + try { + if (!caseId) throw new Error("No Case ID provided."); + const apiUrl = + "https://coretax.intranet.pajak.go.id/casemanagement/api/caseroutinghistory/list", + payload = { + AggregateIdentifier: caseId, + First: 0, + Rows: 1000, + SortField: "RoutingDate", + SortOrder: -1, + Filters: [], + LanguageId: "id-ID", + }, + fetchOptions = { + method: "POST", + headers: getHeaders(caseId), + body: JSON.stringify(payload), + }, + response = await fetch(apiUrl, fetchOptions); + if (!response.ok) { + const errorData = await response.json(); + throw new Error( + `API Error: ${errorData.Message || response.statusText}`, + ); + } + const data = await response.json(); + caseHistoryData = data?.Payload?.Data || []; + loadedHistoryForCaseId = caseId; + + // Fetch CaseRoleTypeCode for each ToWorkflowStepIdentifier + await fetchCaseSubProcessData(caseId); + + populateFilter("history-type-filter", caseHistoryData, "CaseType"); + renderCaseHistoryTable(); + } catch (error) { + handleError(error, responseArea); + } + } + async function fetchCaseSubProcessData(caseId) { + try { + const apiUrl = + "https://coretax.intranet.pajak.go.id/casemanagement/api/casesubprocess/list", + payload = { + First: 0, + Rows: 10000, + Filters: [], + AggregateIdentifier: caseId, + }, + fetchOptions = { + method: "POST", + headers: getHeaders(caseId), + body: JSON.stringify(payload), + }, + response = await fetch(apiUrl, fetchOptions); + + if (!response.ok) { + const errorData = await response.json(); + throw new Error( + `Case SubProcess API Error: ${errorData.Message || response.statusText}`, + ); + } + + const data = await response.json(); + const subProcessData = data?.Payload?.Data || []; + + // Create a lookup map for WorkflowStepIdentifier to CaseRoleTypeCode + caseSubProcessData = {}; + subProcessData.forEach((item) => { + if (item.WorkflowStepIdentifier && item.CaseRoleTypeCode) { + caseSubProcessData[item.WorkflowStepIdentifier] = + item.CaseRoleTypeCode; + } + }); + } catch (error) { + console.error("Error fetching case subprocess data:", error); + caseSubProcessData = {}; + } + } + async function downloadDocument(docId, filename, button) { const originalText = button.textContent; ((button.textContent = "Downloading..."), (button.disabled = !0)); @@ -673,11 +881,9 @@ const table = document.createElement("table"); table.className = "ct-results-table"; const thead = document.createElement("thead"); - return ( - (thead.innerHTML = `