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 = `
- +
@@ -94,299 +125,882 @@
`; - document.body.appendChild(sidebarContainer); - } + // Append to document.body (we are running only in top window so this is safe) + document.body.appendChild(sidebarContainer); + } - // --- RENDER FUNCTIONS --- - function renderMyCasesTable() {const responseArea=document.querySelector("#tab-my-cases .results-container"),filterValue=document.getElementById("cases-status-filter").value,filteredCases="all"===filterValue?allMyCases:allMyCases.filter(e=>e.CaseStatus===filterValue);if(document.getElementById("toggle-cases-btn").textContent="Collapse All",0===filteredCases.length)return void(responseArea.innerHTML='

No cases match the selected filter.

');filteredCases.sort((e,t)=>{const o=(e.CaseTypeName||"").localeCompare(t.CaseTypeName||"");return 0!==o?o:(t.CaseNumber||"").localeCompare(e.CaseNumber||"",void 0,{numeric:!0})});const table=createTable(["Case Number","Taxpayer Name","Case Type","Status","Created Date","Actions"]),tbody=document.createElement("tbody");let currentGroup="";let groupIndex=0;filteredCases.forEach(e=>{if(e.CaseTypeName!==currentGroup){currentGroup=e.CaseTypeName;groupIndex++;tbody.innerHTML+=`${currentGroup||"Uncategorized"}`}const t=document.createElement("tr");t.className=`group-member my-cases-group-${groupIndex}`;const o=e.AggregateIdentifier;t.dataset.id=o,o===selectedCaseId&&t.classList.add("selected");const a=new Date(e.CreatedDate).toLocaleDateString("id-ID"),s=o&&"string"==typeof o&&""!==o.trim(),r=s?"":'disabled title="Action unavailable: Case ID is missing"';let d="";e.CaseTypeName&&e.CaseTypeName.startsWith(REFUND_CASE_PREFIX)&&(d=``),t.innerHTML=` - ${e.CaseNumber||"N/A"} - ${e.MainTaxpayerName||"N/A"} - ${e.CaseTypeName||"N/A"} - ${e.CaseStatus||"N/A"} + // --- RENDER FUNCTIONS --- + function renderMyCasesTable() { + const responseArea = document.querySelector( + "#tab-my-cases .results-container", + ), + filterValue = document.getElementById("cases-status-filter").value, + filteredCases = + "all" === filterValue + ? allMyCases + : allMyCases.filter((e) => e.CaseStatus === filterValue); + if ( + ((document.getElementById("toggle-cases-btn").textContent = + "Collapse All"), + 0 === filteredCases.length) + ) + return void (responseArea.innerHTML = + '

No cases match the selected filter.

'); + filteredCases.sort((e, t) => { + const o = (e.CaseTypeName || "").localeCompare(t.CaseTypeName || ""); + return 0 !== o + ? o + : (t.CaseNumber || "").localeCompare(e.CaseNumber || "", void 0, { + numeric: !0, + }); + }); + const table = createTable([ + "Case Number", + "Taxpayer Name", + "Case Type", + "Status", + "Created Date", + "Actions", + ]), + tbody = document.createElement("tbody"); + let currentGroup = ""; + let groupIndex = 0; + (filteredCases.forEach((e) => { + if (e.CaseTypeName !== currentGroup) { + currentGroup = e.CaseTypeName; + groupIndex++; + tbody.innerHTML += `${currentGroup || "Uncategorized"}`; + } + const t = document.createElement("tr"); + t.className = `group-member my-cases-group-${groupIndex}`; + const o = e.AggregateIdentifier; + ((t.dataset.id = o), o === selectedCaseId && t.classList.add("selected")); + const a = new Date(e.CreatedDate).toLocaleDateString("id-ID"), + s = o && "string" == typeof o && "" !== o.trim(), + r = s ? "" : 'disabled title="Action unavailable: Case ID is missing"'; + let d = ""; + (e.CaseTypeName && + e.CaseTypeName.startsWith(REFUND_CASE_PREFIX) && + (d = ``), + (t.innerHTML = ` + ${e.CaseNumber || "N/A"} + ${e.MainTaxpayerName || "N/A"} + ${e.CaseTypeName || "N/A"} + ${e.CaseStatus || "N/A"} ${a} Open ${d} - `,tbody.appendChild(t)}),table.appendChild(tbody),responseArea.innerHTML="",responseArea.appendChild(table),tbody.addEventListener("click",handleGroupToggle),tbody.addEventListener("click",handleCaseSelection)} - function renderCaseDocumentsTable() {const responseArea=document.querySelector("#tab-docs .results-container"),filterValue=document.getElementById("docs-status-filter").value,filteredDocs="all"===filterValue?allCaseDocuments:allCaseDocuments.filter(e=>e.DocumentStatus===filterValue);if(document.getElementById("toggle-docs-btn").textContent="Collapse All",0===filteredDocs.length)return void(responseArea.innerHTML='

No documents found or match the selected filter.

');filteredDocs.sort((e,t)=>(e.DocumentTypeCode||"").localeCompare(t.DocumentTypeCode||""));const table=createTable(["Letter Number","File Name","Status","Date","Actions"]),tbody=document.createElement("tbody");let currentGroup="";let groupIndex=0;filteredDocs.forEach(e=>{if(e.DocumentTypeCode!==currentGroup){currentGroup=e.DocumentTypeCode;groupIndex++;tbody.innerHTML+=`${currentGroup||"Uncategorized"}`}const t=e.DocumentDate?new Date(e.DocumentDate).toLocaleDateString("id-ID"):"N/A";tbody.innerHTML+=` + `), + tbody.appendChild(t)); + }), + table.appendChild(tbody), + (responseArea.innerHTML = ""), + responseArea.appendChild(table), + tbody.addEventListener("click", handleGroupToggle), + tbody.addEventListener("click", handleCaseSelection)); + } + function renderCaseDocumentsTable() { + const responseArea = document.querySelector("#tab-docs .results-container"), + filterValue = document.getElementById("docs-status-filter").value, + filteredDocs = + "all" === filterValue + ? allCaseDocuments + : allCaseDocuments.filter((e) => e.DocumentStatus === filterValue); + if ( + ((document.getElementById("toggle-docs-btn").textContent = + "Collapse All"), + 0 === filteredDocs.length) + ) + return void (responseArea.innerHTML = + '

No documents found or match the selected filter.

'); + filteredDocs.sort((e, t) => + (e.DocumentTypeCode || "").localeCompare(t.DocumentTypeCode || ""), + ); + const table = createTable([ + "Letter Number", + "File Name", + "Status", + "Date", + "Actions", + ]), + tbody = document.createElement("tbody"); + let currentGroup = ""; + let groupIndex = 0; + (filteredDocs.forEach((e) => { + if (e.DocumentTypeCode !== currentGroup) { + currentGroup = e.DocumentTypeCode; + groupIndex++; + tbody.innerHTML += `${currentGroup || "Uncategorized"}`; + } + const t = e.DocumentDate + ? new Date(e.DocumentDate).toLocaleDateString("id-ID") + : "N/A"; + tbody.innerHTML += ` - ${e.LetterNumber||"N/A"} - ${e.FileName||"N/A"} - ${e.DocumentStatus||"N/A"} + ${e.LetterNumber || "N/A"} + ${e.FileName || "N/A"} + ${e.DocumentStatus || "N/A"} ${t} - `}),table.appendChild(tbody),responseArea.innerHTML="",responseArea.appendChild(table),tbody.addEventListener("click",handleGroupToggle),tbody.addEventListener("click",handleDocumentAction)} - function renderCaseUsersTable(){const responseArea=document.querySelector("#tab-users .results-container"),filterValue=document.getElementById("users-role-filter").value,filteredUsers="all"===filterValue?allCaseUsers:allCaseUsers.filter(e=>e.CaseRoleType===filterValue);if(0===filteredUsers.length)return void(responseArea.innerHTML='

No users found or match the selected filter.

');filteredUsers.sort((e,t)=>(e.FullName||"").localeCompare(t.FullName||""));const table=createTable(["Full Name","NIP","Position","Office","Case Role"]),tbody=document.createElement("tbody");filteredUsers.forEach(e=>{tbody.innerHTML+=` + `; + }), + table.appendChild(tbody), + (responseArea.innerHTML = ""), + responseArea.appendChild(table), + tbody.addEventListener("click", handleGroupToggle), + tbody.addEventListener("click", handleDocumentAction)); + } + function renderCaseUsersTable() { + const responseArea = document.querySelector( + "#tab-users .results-container", + ), + filterValue = document.getElementById("users-role-filter").value, + filteredUsers = + "all" === filterValue + ? allCaseUsers + : allCaseUsers.filter((e) => e.CaseRoleType === filterValue); + if (0 === filteredUsers.length) + return void (responseArea.innerHTML = + '

No users found or match the selected filter.

'); + filteredUsers.sort((e, t) => + (e.FullName || "").localeCompare(t.FullName || ""), + ); + const table = createTable([ + "Full Name", + "NIP", + "Position", + "Office", + "Case Role", + ]), + tbody = document.createElement("tbody"); + (filteredUsers.forEach((e) => { + tbody.innerHTML += ` - ${e.FullName||"N/A"} - ${e.Nip||"N/A"} - ${e.Jabatan||"N/A"} - ${e.OfficeName||"N/A"} - ${e.CaseRoleType||"N/A"} - `}),table.appendChild(tbody),responseArea.innerHTML="",responseArea.appendChild(table)} - function renderRefundReviewTable() {const responseArea=document.querySelector("#tab-refund .results-container"),filterValue=document.getElementById("refund-reported-filter").value;document.getElementById("refund-download-btn").disabled=!refundReviewData||0===refundReviewData.length,document.getElementById("toggle-refund-btn").textContent="Collapse All";let dataToRender=refundReviewData;"all"!==filterValue&&(dataToRender=refundReviewData.filter(e=>e.ReportedBySeller===("true"===filterValue))),filteredRefundData=dataToRender;if(!dataToRender||0===dataToRender.length)return void(responseArea.innerHTML='

No refund review data matches the filter.

');dataToRender.sort((e,t)=>{const o=(e.Tin||"")+(e.Name||""),a=(t.Tin||"")+(t.Name||"");return o.localeCompare(a)});const table=createTable(["Doc Number","Date","Selling Price","VAT Paid","STLG Paid","Trans Code","Reported"]),tbody=document.createElement("tbody");let currentGroupKey="";let groupIndex=0;responseArea.innerHTML="",table.appendChild(tbody),responseArea.appendChild(table),dataToRender.forEach(e=>{const t=(e.Tin||"")+(e.Name||"");if(t!==currentGroupKey){currentGroupKey=t,groupIndex++;const o=tbody.insertRow();o.className="group-header expanded",o.dataset.groupId=`refund-group-${groupIndex}`;const a=o.insertCell();a.colSpan=7,a.innerHTML=` + ${e.FullName || "N/A"} + ${e.Nip || "N/A"} + ${e.Jabatan || "N/A"} + ${e.OfficeName || "N/A"} + ${e.CaseRoleType || "N/A"} + `; + }), + table.appendChild(tbody), + (responseArea.innerHTML = ""), + responseArea.appendChild(table)); + } + function renderRefundReviewTable() { + const responseArea = document.querySelector( + "#tab-refund .results-container", + ), + filterValue = document.getElementById("refund-reported-filter").value; + ((document.getElementById("refund-download-btn").disabled = + !refundReviewData || 0 === refundReviewData.length), + (document.getElementById("toggle-refund-btn").textContent = + "Collapse All")); + let dataToRender = refundReviewData; + ("all" !== filterValue && + (dataToRender = refundReviewData.filter( + (e) => e.ReportedBySeller === ("true" === filterValue), + )), + (filteredRefundData = dataToRender)); + if (!dataToRender || 0 === dataToRender.length) + return void (responseArea.innerHTML = + '

No refund review data matches the filter.

'); + dataToRender.sort((e, t) => { + const o = (e.Tin || "") + (e.Name || ""), + a = (t.Tin || "") + (t.Name || ""); + return o.localeCompare(a); + }); + const table = createTable([ + "Doc Number", + "Date", + "Selling Price", + "VAT Paid", + "STLG Paid", + "Trans Code", + "Reported", + ]), + tbody = document.createElement("tbody"); + let currentGroupKey = ""; + let groupIndex = 0; + ((responseArea.innerHTML = ""), + table.appendChild(tbody), + responseArea.appendChild(table), + dataToRender.forEach((e) => { + const t = (e.Tin || "") + (e.Name || ""); + if (t !== currentGroupKey) { + ((currentGroupKey = t), groupIndex++); + const o = tbody.insertRow(); + ((o.className = "group-header expanded"), + (o.dataset.groupId = `refund-group-${groupIndex}`)); + const a = o.insertCell(); + ((a.colSpan = 7), + (a.innerHTML = ` -
${e.Name||"Unknown Name"}
-
${e.Tin||"Unknown TIN"}
-
`}const o=tbody.insertRow();o.className=`group-member refund-group-${groupIndex}`;const a=e.DocumentDate?new Date(e.DocumentDate).toLocaleDateString("id-ID"):"N/A",s=e.ReportedBySeller?'':'';o.insertCell().textContent=e.DocumentNumber||"N/A",o.insertCell().textContent=a;const r=o.insertCell();r.innerHTML=`
Rp${(e.SellingPrice||0).toLocaleString("id-ID")}
`;const d=o.insertCell();d.innerHTML=`
Rp${(e.VatPaid||0).toLocaleString("id-ID")}
`;const n=o.insertCell();n.innerHTML=`
Rp${(e.StlgPaid||0).toLocaleString("id-ID")}
`,o.insertCell().textContent=e.TransactionCode||"N/A";const c=o.insertCell();c.className="reported-cell",c.innerHTML=s}),tbody.addEventListener("click",handleGroupToggle)} - - // --- DATA FETCHING FUNCTIONS --- - async function fetchMyCases(){const responseArea=document.querySelector("#tab-my-cases .results-container");try{const authToken=getAuthToken(),apiUrl="https://coretax.intranet.pajak.go.id/casemanagement/api/caselist/mycases",fetchOptions={method:"POST",headers:getHeaders(),body:JSON.stringify({})},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();allMyCases=data?.Payload?.Data||[],populateFilter("cases-status-filter",allMyCases,"CaseStatus");const casesFilter=document.getElementById("cases-status-filter");Array.from(casesFilter.options).some(e=>e.value===DEFAULT_CASES_FILTER)&&(casesFilter.value=DEFAULT_CASES_FILTER),renderMyCasesTable()}catch(error){handleError(error,responseArea)}} - async function fetchCaseDocuments(caseId){const responseArea=document.querySelector("#tab-docs .results-container");responseArea.innerHTML='

Loading documents...

';try{if(!caseId)throw new Error("No Case ID provided.");const apiUrl="https://coretax.intranet.pajak.go.id/casemanagement/api/casedocument/list",fetchOptions={method:"POST",headers:getHeaders(caseId),body:JSON.stringify({AggregateIdentifier:caseId})},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();allCaseDocuments=data?.Payload?.Data||[],loadedDocsForCaseId=caseId,populateFilter("docs-status-filter",allCaseDocuments,"DocumentStatus"),renderCaseDocumentsTable()}catch(error){handleError(error,responseArea)}} - async function fetchCaseUsers(caseId){const responseArea=document.querySelector("#tab-users .results-container");responseArea.innerHTML='

Loading users...

';try{if(!caseId)throw new Error("No Case ID provided.");const apiUrl="https://coretax.intranet.pajak.go.id/casemanagement/api/caseuser/list",payload={AggregateIdentifier:caseId,First:0,Rows:200,SortField:"",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();allCaseUsers=data?.Payload?.Data||[],loadedUsersForCaseId=caseId,populateFilter("users-role-filter",allCaseUsers,"CaseRoleType"),renderCaseUsersTable()}catch(error){handleError(error,responseArea)}} - async function downloadDocument(docId, filename, button) {const originalText=button.textContent;button.textContent="Downloading...",button.disabled=!0;try{const apiUrl="https://coretax.intranet.pajak.go.id/documentmanagement/api/download",payload={DocumentAggregateIdentifier:docId,IsDocumentCases:!1,IsNeedWatermark:null},fetchOptions={method:"POST",headers:getHeaders(),body:JSON.stringify(payload)},response=await fetch(apiUrl,fetchOptions);if(!response.ok)try{const errorData=await response.json();throw new Error(`API Error: ${errorData.Message||response.statusText}`)}catch(e){throw new Error(`API request failed! Status: ${response.statusText}`)}const blob=await response.blob(),url=window.URL.createObjectURL(blob),a=document.createElement("a");a.style.display="none",a.href=url,a.download=filename||"download.pdf",document.body.appendChild(a),a.click(),window.URL.revokeObjectURL(url),a.remove()}catch(error){alert(`Download failed: ${error.message}`)}finally{button.textContent=originalText,button.disabled=!1}} - async function fetchSubProcessId(caseId) {const apiUrl="https://coretax.intranet.pajak.go.id/casemanagement/api/caserouting/view",payload={AggregateIdentifier:caseId,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(`Sub Process ID API Error: ${errorData.Message||response.statusText}`)}const data=await response.json(),firstResult=data?.Payload?.[0];if(!firstResult||!firstResult.SubProcessIdentifier)throw new Error("Could not find a 'SubProcessIdentifier' in the caserouting/view response.");return firstResult.SubProcessIdentifier} - - // NEW: This function now includes the fallback logic. - async function fetchC02FormDetail(caseId, subProcessId) { - const payload = { caseAggregateIdentifier: caseId, CaseSubProcessIdentifier: subProcessId }; - const fetchOptions = { method: "POST", headers: getHeaders(caseId), body: JSON.stringify(payload) }; - - // 1. Primary API Attempt - const primaryApiUrl = "https://coretax.intranet.pajak.go.id/casecomponentspayment/api/c02form014/c02form009detail/current"; - try { - const response = await fetch(primaryApiUrl, fetchOptions); - if (response.ok) { - const data = await response.json(); - const reference = data?.Payload?.[0]?.Reference; - if (reference) return reference; // Success on primary API - } - } catch (error) { - console.warn("Primary API for C02Form failed, trying fallback.", error); +
${e.Name || "Unknown Name"}
+
${e.Tin || "Unknown TIN"}
+ `)); } + const o = tbody.insertRow(); + o.className = `group-member refund-group-${groupIndex}`; + const a = e.DocumentDate + ? new Date(e.DocumentDate).toLocaleDateString("id-ID") + : "N/A", + s = e.ReportedBySeller + ? '' + : ''; + ((o.insertCell().textContent = e.DocumentNumber || "N/A"), + (o.insertCell().textContent = a)); + const r = o.insertCell(); + r.innerHTML = `
Rp${(e.SellingPrice || 0).toLocaleString("id-ID")}
`; + const d = o.insertCell(); + d.innerHTML = `
Rp${(e.VatPaid || 0).toLocaleString("id-ID")}
`; + const n = o.insertCell(); + ((n.innerHTML = `
Rp${(e.StlgPaid || 0).toLocaleString("id-ID")}
`), + (o.insertCell().textContent = e.TransactionCode || "N/A")); + const c = o.insertCell(); + ((c.className = "reported-cell"), (c.innerHTML = s)); + }), + tbody.addEventListener("click", handleGroupToggle)); + } - // 2. Fallback API Attempt - const responseArea = document.querySelector('#tab-refund .results-container'); - responseArea.innerHTML = `

Step 2/3: Fetching reference number (trying fallback API)...

`; - const fallbackApiUrl = "https://coretax.intranet.pajak.go.id/casecomponentspayment/api/c02form014/view"; - const fallbackResponse = await fetch(fallbackApiUrl, fetchOptions); - if (!fallbackResponse.ok) { - const errorData = await fallbackResponse.json(); - throw new Error(`C02Form Detail API Error (Fallback): ${errorData.Message || fallbackResponse.statusText}`); - } - const fallbackData = await fallbackResponse.json(); - const fallbackReference = fallbackData?.Payload?.Details?.[0]?.Reference; - if (fallbackReference) return fallbackReference; // Success on fallback API - - // 3. If both fail - throw new Error("Could not find a 'Reference' number in either C02Form Detail response."); + // --- DATA FETCHING FUNCTIONS --- + async function fetchMyCases() { + const responseArea = document.querySelector( + "#tab-my-cases .results-container", + ); + try { + const authToken = getAuthToken(), + apiUrl = + "https://coretax.intranet.pajak.go.id/casemanagement/api/caselist/mycases", + fetchOptions = { + method: "POST", + headers: getHeaders(), + body: JSON.stringify({}), + }, + 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(); + ((allMyCases = data?.Payload?.Data || []), + populateFilter("cases-status-filter", allMyCases, "CaseStatus")); + const casesFilter = document.getElementById("cases-status-filter"); + (Array.from(casesFilter.options).some( + (e) => e.value === DEFAULT_CASES_FILTER, + ) && (casesFilter.value = DEFAULT_CASES_FILTER), + renderMyCasesTable()); + } catch (error) { + handleError(error, responseArea); } - - async function fetchRefundReview(caseId, refNumber) {const responseArea=document.querySelector("#tab-refund .results-container");try{const apiUrl="https://coretax.intranet.pajak.go.id/casecomponentspayment/api/refundprocessreview/get-detailed-review",payload={CaseAggregateIdentifier:caseId,RevenueCode:"411211",TaxPaymentCode:"100",TaxReturnType:"VAT_VATR",ReferenceNumber:refNumber},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(`Refund Review API Error: ${errorData.Message||response.statusText}`)}const data=await response.json();refundReviewData=data?.Payload||[],populateBooleanFilter("refund-reported-filter"),renderRefundReviewTable()}catch(error){handleError(error,responseArea)}} + } + async function fetchCaseDocuments(caseId) { + const responseArea = document.querySelector("#tab-docs .results-container"); + responseArea.innerHTML = + '

Loading documents...

'; + try { + if (!caseId) throw new Error("No Case ID provided."); + const apiUrl = + "https://coretax.intranet.pajak.go.id/casemanagement/api/casedocument/list", + fetchOptions = { + method: "POST", + headers: getHeaders(caseId), + body: JSON.stringify({ AggregateIdentifier: caseId }), + }, + 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(); + ((allCaseDocuments = data?.Payload?.Data || []), + (loadedDocsForCaseId = caseId), + populateFilter( + "docs-status-filter", + allCaseDocuments, + "DocumentStatus", + ), + renderCaseDocumentsTable()); + } catch (error) { + handleError(error, responseArea); + } + } + async function fetchCaseUsers(caseId) { + const responseArea = document.querySelector( + "#tab-users .results-container", + ); + responseArea.innerHTML = + '

Loading users...

'; + try { + if (!caseId) throw new Error("No Case ID provided."); + const apiUrl = + "https://coretax.intranet.pajak.go.id/casemanagement/api/caseuser/list", + payload = { + AggregateIdentifier: caseId, + First: 0, + Rows: 200, + SortField: "", + 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(); + ((allCaseUsers = data?.Payload?.Data || []), + (loadedUsersForCaseId = caseId), + populateFilter("users-role-filter", allCaseUsers, "CaseRoleType"), + renderCaseUsersTable()); + } catch (error) { + handleError(error, responseArea); + } + } + async function downloadDocument(docId, filename, button) { + const originalText = button.textContent; + ((button.textContent = "Downloading..."), (button.disabled = !0)); + try { + const apiUrl = + "https://coretax.intranet.pajak.go.id/documentmanagement/api/download", + payload = { + DocumentAggregateIdentifier: docId, + IsDocumentCases: !1, + IsNeedWatermark: null, + }, + fetchOptions = { + method: "POST", + headers: getHeaders(), + body: JSON.stringify(payload), + }, + response = await fetch(apiUrl, fetchOptions); + if (!response.ok) + try { + const errorData = await response.json(); + throw new Error( + `API Error: ${errorData.Message || response.statusText}`, + ); + } catch (e) { + throw new Error(`API request failed! Status: ${response.statusText}`); + } + const blob = await response.blob(), + url = window.URL.createObjectURL(blob), + a = document.createElement("a"); + ((a.style.display = "none"), + (a.href = url), + (a.download = filename || "download.pdf"), + document.body.appendChild(a), + a.click(), + window.URL.revokeObjectURL(url), + a.remove()); + } catch (error) { + alert(`Download failed: ${error.message}`); + } finally { + ((button.textContent = originalText), (button.disabled = !1)); + } + } + async function fetchSubProcessId(caseId) { + const apiUrl = + "https://coretax.intranet.pajak.go.id/casemanagement/api/caserouting/view", + payload = { AggregateIdentifier: caseId, 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( + `Sub Process ID API Error: ${errorData.Message || response.statusText}`, + ); + } + const data = await response.json(), + firstResult = data?.Payload?.[0]; + if (!firstResult || !firstResult.SubProcessIdentifier) + throw new Error( + "Could not find a 'SubProcessIdentifier' in the caserouting/view response.", + ); + return firstResult.SubProcessIdentifier; + } - // --- HELPER FUNCTIONS --- - function getHeaders(caseId=null){const authToken=getAuthToken(),requestFromUrl=caseId?`https://coretax.intranet.pajak.go.id/case-management/id-ID/case-overview/${caseId}`:window.location.href;return{accept:"application/json, text/plain, */*",authorization:`Bearer ${authToken}`,"content-type":"application/json",languageid:"id-ID",request_from:requestFromUrl}} - function populateFilter(selectId,data,key){const select=document.getElementById(selectId);if(!select)return;const values=[...new Set(data.map(e=>e[key]).filter(Boolean))];select.innerHTML='',values.sort().forEach(e=>{select.innerHTML+=``})} - function populateBooleanFilter(selectId){const e=document.getElementById(selectId);e&&(e.innerHTML='')} - function getAuthToken(){const userDataString=localStorage.getItem(AUTH_STORAGE_KEY),userData=userDataString?JSON.parse(userDataString):null,authToken=userData?.access_token;if(!authToken)throw new Error("Authorization Token not found.");return authToken} - function createTable(headers){const table=document.createElement("table");table.className="ct-results-table";const thead=document.createElement("thead");return thead.innerHTML=`${headers.map(e=>`${e}`).join("")}`,table.appendChild(thead),table} - function handleError(error,area){console.error("Userscript Error:",error);let errorHtml;error.message.includes("Authorization Token not found")?errorHtml=` + // NEW: This function now includes the fallback logic. + async function fetchC02FormDetail(caseId, subProcessId) { + const payload = { + caseAggregateIdentifier: caseId, + CaseSubProcessIdentifier: subProcessId, + }; + const fetchOptions = { + method: "POST", + headers: getHeaders(caseId), + body: JSON.stringify(payload), + }; + + // 1. Primary API Attempt + const primaryApiUrl = + "https://coretax.intranet.pajak.go.id/casecomponentspayment/api/c02form014/c02form009detail/current"; + try { + const response = await fetch(primaryApiUrl, fetchOptions); + if (response.ok) { + const data = await response.json(); + const reference = data?.Payload?.[0]?.Reference; + if (reference) return reference; // Success on primary API + } + } catch (error) { + console.warn("Primary API for C02Form failed, trying fallback.", error); + } + + // 2. Fallback API Attempt + const responseArea = document.querySelector( + "#tab-refund .results-container", + ); + responseArea.innerHTML = `

Step 2/3: Fetching reference number (trying fallback API)...

`; + const fallbackApiUrl = + "https://coretax.intranet.pajak.go.id/casecomponentspayment/api/c02form014/view"; + const fallbackResponse = await fetch(fallbackApiUrl, fetchOptions); + if (!fallbackResponse.ok) { + const errorData = await fallbackResponse.json(); + throw new Error( + `C02Form Detail API Error (Fallback): ${errorData.Message || fallbackResponse.statusText}`, + ); + } + const fallbackData = await fallbackResponse.json(); + const fallbackReference = fallbackData?.Payload?.Details?.[0]?.Reference; + if (fallbackReference) return fallbackReference; // Success on fallback API + + // 3. If both fail + throw new Error( + "Could not find a 'Reference' number in either C02Form Detail response.", + ); + } + + async function fetchRefundReview(caseId, refNumber) { + const responseArea = document.querySelector( + "#tab-refund .results-container", + ); + try { + const apiUrl = + "https://coretax.intranet.pajak.go.id/casecomponentspayment/api/refundprocessreview/get-detailed-review", + payload = { + CaseAggregateIdentifier: caseId, + RevenueCode: "411211", + TaxPaymentCode: "100", + TaxReturnType: "VAT_VATR", + ReferenceNumber: refNumber, + }, + 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( + `Refund Review API Error: ${errorData.Message || response.statusText}`, + ); + } + const data = await response.json(); + ((refundReviewData = data?.Payload || []), + populateBooleanFilter("refund-reported-filter"), + renderRefundReviewTable()); + } catch (error) { + handleError(error, responseArea); + } + } + + // --- HELPER FUNCTIONS --- + function getHeaders(caseId = null) { + const authToken = getAuthToken(), + requestFromUrl = caseId + ? `https://coretax.intranet.pajak.go.id/case-management/id-ID/case-overview/${caseId}` + : window.location.href; + return { + accept: "application/json, text/plain, */*", + authorization: `Bearer ${authToken}`, + "content-type": "application/json", + languageid: "id-ID", + request_from: requestFromUrl, + }; + } + function populateFilter(selectId, data, key) { + const select = document.getElementById(selectId); + if (!select) return; + const values = [...new Set(data.map((e) => e[key]).filter(Boolean))]; + ((select.innerHTML = ''), + values.sort().forEach((e) => { + select.innerHTML += ``; + })); + } + function populateBooleanFilter(selectId) { + const e = document.getElementById(selectId); + e && + (e.innerHTML = + ''); + } + function getAuthToken() { + const userDataString = localStorage.getItem(AUTH_STORAGE_KEY), + userData = userDataString ? JSON.parse(userDataString) : null, + authToken = userData?.access_token; + if (!authToken) throw new Error("Authorization Token not found."); + return authToken; + } + function createTable(headers) { + const table = document.createElement("table"); + table.className = "ct-results-table"; + const thead = document.createElement("thead"); + return ( + (thead.innerHTML = `${headers.map((e) => `${e}`).join("")}`), + table.appendChild(thead), + table + ); + } + function handleError(error, area) { + console.error("Userscript Error:", error); + let errorHtml; + (error.message.includes("Authorization Token not found") + ? (errorHtml = `
Session Expired or Token Not Found

Please refresh the page to log in again.

-
`:errorHtml=`

An error occurred:
${error.message}

`,area.innerHTML=errorHtml;const refreshBtn=area.querySelector("#auth-refresh-btn");refreshBtn&&refreshBtn.addEventListener("click",()=>window.location.reload())} - function updateHeader(caseObject) { - const titleEl = document.getElementById('ct-header-title'); - const subtitleEl = document.getElementById('ct-header-subtitle'); - const actionsContainer = document.getElementById('ct-header-actions'); - actionsContainer.innerHTML = ''; // Clear previous buttons +
`) + : (errorHtml = `

An error occurred:
${error.message}

`), + (area.innerHTML = errorHtml)); + const refreshBtn = area.querySelector("#auth-refresh-btn"); + refreshBtn && + refreshBtn.addEventListener("click", () => window.location.reload()); + } + function updateHeader(caseObject) { + const titleEl = document.getElementById("ct-header-title"); + const subtitleEl = document.getElementById("ct-header-subtitle"); + const actionsContainer = document.getElementById("ct-header-actions"); + actionsContainer.innerHTML = ""; // Clear previous buttons - if (caseObject) { - titleEl.textContent = caseObject.MainTaxpayerName || 'N/A'; - subtitleEl.textContent = caseObject.CaseNumber || 'N/A'; - const caseId = caseObject.AggregateIdentifier; - const hasValidId = caseId && typeof caseId === 'string' && caseId.trim() !== ''; + if (caseObject) { + titleEl.textContent = caseObject.MainTaxpayerName || "N/A"; + subtitleEl.textContent = caseObject.CaseNumber || "N/A"; + const caseId = caseObject.AggregateIdentifier; + const hasValidId = + caseId && typeof caseId === "string" && caseId.trim() !== ""; - const openBtn = document.createElement('a'); - openBtn.href = `https://coretax.intranet.pajak.go.id/case-management/id-ID/case-overview/${caseId}`; - openBtn.className = 'action-btn open-case'; - openBtn.textContent = 'Open'; - if (!hasValidId) openBtn.disabled = true; + const openBtn = document.createElement("a"); + openBtn.href = `https://coretax.intranet.pajak.go.id/case-management/id-ID/case-overview/${caseId}`; + openBtn.className = "action-btn open-case"; + openBtn.textContent = "Open"; + if (!hasValidId) openBtn.disabled = true; - const docsBtn = document.createElement('button'); - docsBtn.className = 'action-btn view-docs'; - docsBtn.textContent = 'View Docs'; - if (!hasValidId) docsBtn.disabled = true; - docsBtn.addEventListener('click', () => switchTab('tab-docs')); + const docsBtn = document.createElement("button"); + docsBtn.className = "action-btn view-docs"; + docsBtn.textContent = "View Docs"; + if (!hasValidId) docsBtn.disabled = true; + docsBtn.addEventListener("click", () => switchTab("tab-docs")); - const usersBtn = document.createElement('button'); - usersBtn.className = 'action-btn view-users'; - usersBtn.textContent = 'View Users'; - if (!hasValidId) usersBtn.disabled = true; - usersBtn.addEventListener('click', () => switchTab('tab-users')); + const usersBtn = document.createElement("button"); + usersBtn.className = "action-btn view-users"; + usersBtn.textContent = "View Users"; + if (!hasValidId) usersBtn.disabled = true; + usersBtn.addEventListener("click", () => switchTab("tab-users")); - actionsContainer.append(openBtn, docsBtn, usersBtn); + actionsContainer.append(openBtn, docsBtn, usersBtn); - if (caseObject.CaseTypeName && caseObject.CaseTypeName.startsWith(REFUND_CASE_PREFIX)) { - const refundBtn = document.createElement('button'); - refundBtn.className = 'action-btn review-refund-case'; - refundBtn.textContent = 'Refund Review'; - actionsContainer.appendChild(refundBtn); - refundBtn.addEventListener('click', () => startRefundReviewProcess(caseId, refundBtn)); - } + if ( + caseObject.CaseTypeName && + caseObject.CaseTypeName.startsWith(REFUND_CASE_PREFIX) + ) { + const refundBtn = document.createElement("button"); + refundBtn.className = "action-btn review-refund-case"; + refundBtn.textContent = "Refund Review"; + actionsContainer.appendChild(refundBtn); + refundBtn.addEventListener("click", () => + startRefundReviewProcess(caseId, refundBtn), + ); + } + } 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; + } + } + + 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.

+
@@ -298,6 +309,118 @@ (responseArea.innerHTML = ""), responseArea.appendChild(table)); } + function renderCaseHistoryTable() { + const responseArea = document.querySelector( + "#tab-history .results-container", + ); + if (!caseHistoryData || caseHistoryData.length === 0) { + responseArea.innerHTML = + '

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 += `${caseType} (${groupItems.length} items)`; + + // Add current role type at the top of each group + // Find the most recent item (with latest RoutingDate) + const mostRecentItem = groupItems.reduce((latest, current) => { + const latestDate = new Date(latest.RoutingDate); + const currentDate = new Date(current.RoutingDate); + return currentDate > latestDate ? current : latest; + }); + const currentCaseRoleTypeCode = mostRecentItem.ToWorkflowStepIdentifier + ? caseSubProcessData[mostRecentItem.ToWorkflowStepIdentifier] + : null; + + // Always add the current role row, even if CaseRoleTypeCode is not available + const currentRoleRow = document.createElement("tr"); + currentRoleRow.className = `group-member history-group-${sanitizedCaseType}`; + + // Empty Routing Date + const emptyDateCell = document.createElement("td"); + emptyDateCell.textContent = ""; + currentRoleRow.appendChild(emptyDateCell); + + // CaseRoleTypeCode in Performed By column + const roleCell = document.createElement("td"); + if (currentCaseRoleTypeCode) { + roleCell.innerHTML = `Current Role:
${currentCaseRoleTypeCode}`; + } else { + roleCell.innerHTML = `Current Role:
Not available`; + } + currentRoleRow.appendChild(roleCell); + + // Last To Step in Workflow Step column + const workflowStepCell = document.createElement("td"); + workflowStepCell.textContent = mostRecentItem.ToWorkflowStep || "-"; + currentRoleRow.appendChild(workflowStepCell); + + tbody.appendChild(currentRoleRow); + + // Create member rows for this group + groupItems.forEach((item) => { + const row = document.createElement("tr"); + row.className = `group-member history-group-${sanitizedCaseType}`; + + // Format RoutingDate + const routingDate = new Date(item.RoutingDate); + const dateCell = document.createElement("td"); + dateCell.textContent = routingDate.toLocaleString("id-ID"); + row.appendChild(dateCell); + + // PerformedByUser + const performedByCell = document.createElement("td"); + performedByCell.textContent = item.PerformedByUser || "-"; + row.appendChild(performedByCell); + + // FromWorkflowStep (renamed to Workflow Step) + const workflowStepCell = document.createElement("td"); + workflowStepCell.textContent = item.FromWorkflowStep || "-"; + row.appendChild(workflowStepCell); + + tbody.appendChild(row); + }); + }); + + responseArea.innerHTML = ""; + table.appendChild(tbody); + responseArea.appendChild(table); + tbody.addEventListener("click", handleGroupToggle); + } function renderRefundReviewTable() { const responseArea = document.querySelector( "#tab-refund .results-container", @@ -481,6 +604,91 @@ handleError(error, responseArea); } } + async function fetchCaseHistory(caseId) { + const responseArea = document.querySelector( + "#tab-history .results-container", + ); + responseArea.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 = `${headers.map((e) => `${e}`).join("")}`), - table.appendChild(thead), - table - ); + thead.innerHTML = `${headers.map((e) => `${e}`).join("")}`; + table.appendChild(thead); + return table; } function handleError(error, area) { console.error("Userscript Error:", error); @@ -823,7 +1029,7 @@ headerRow.classList.toggle("expanded"); const isCollapsed = headerRow.classList.contains("collapsed"); const tableBody = headerRow.parentElement; - const memberRows = tableBody.querySelectorAll(`.group-member.${groupId}`); + const memberRows = tableBody.querySelectorAll(`tr.group-member.${groupId}`); memberRows.forEach((row) => { row.style.display = isCollapsed ? "none" : "table-row"; }); @@ -837,14 +1043,18 @@ .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)); + "tab-docs" === tabId && + selectedCaseId && + selectedCaseId !== loadedDocsForCaseId && + fetchCaseDocuments(selectedCaseId), + "tab-users" === tabId && + selectedCaseId && + selectedCaseId !== loadedUsersForCaseId && + fetchCaseUsers(selectedCaseId), + "tab-history" === tabId && + selectedCaseId && + selectedCaseId !== loadedHistoryForCaseId && + fetchCaseHistory(selectedCaseId)); } function downloadRefundExcel() { if (!filteredRefundData || filteredRefundData.length === 0) { @@ -992,6 +1202,10 @@ if (usersFilterEl) usersFilterEl.addEventListener("change", renderCaseUsersTable); + const historyFilterEl = document.getElementById("history-type-filter"); + if (historyFilterEl) + historyFilterEl.addEventListener("change", renderCaseHistoryTable); + const refundFilterEl = document.getElementById("refund-reported-filter"); if (refundFilterEl) refundFilterEl.addEventListener("change", renderRefundReviewTable); @@ -1018,6 +1232,12 @@ toggleAllGroups("#tab-refund .results-container", e.target), ); + const toggleHistoryBtn = document.getElementById("toggle-history-btn"); + if (toggleHistoryBtn) + toggleHistoryBtn.addEventListener("click", (e) => + toggleAllGroups("#tab-history .results-container", e.target), + ); + // initial load fetchMyCases(); } -- 2.49.1