// ==UserScript== // @name CoreTabs1 // @namespace https://git.diasbaskara.id/diasbaskara/userscripts/ // @version 0.5 // @description Manage your cases easily. // @author Dias Baskara // @match https://coretax.intranet.pajak.go.id/* // @grant GM_addStyle // @require https://cdn.sheetjs.com/xlsx-latest/package/dist/xlsx.full.min.js // @run-at document-idle // ==/UserScript== (function () { "use strict"; // 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; } // 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, }); // --- 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 = [], caseHistoryData = [], caseSubProcessData = {}, caseUsersRoleMap = {}, refundReviewData = [], filteredRefundData = [], selectedCaseId = null, loadedDocsForCaseId = null, loadedUsersForCaseId = null, loadedHistoryForCaseId = 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-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; } #ct-header-text { flex-grow: 1; min-width: 0; } #ct-header-title { font-size: 16px; font-weight: bold; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } #ct-header-subtitle { font-size: 12px; color: #adb5bd; } #ct-header-actions { margin-left: auto; flex-shrink: 0; display: flex; gap: 5px; } #ct-tab-bar{display:flex;background-color:#e9ecef;border-bottom:1px solid #ccc;flex-shrink:0}.ct-tab-button{padding:10px 15px;border:none;background-color:transparent;cursor:pointer;font-size:14px;border-bottom:3px solid transparent;transition:background-color .2s,border-color .2s}.ct-tab-button:hover{background-color:#dcdcdc}.ct-tab-button.active{border-bottom:3px solid #0056b3;font-weight:700;background-color:#fff}#ct-tab-content-area{padding:15px;flex-grow:1;overflow:hidden;display:flex;flex-direction:column}.ct-tab-panel{display:none;flex-grow:1;overflow:hidden;flex-direction:column}.ct-tab-panel.active{display:flex} .filter-container{margin-bottom:10px;flex-shrink:0;display:flex;gap:15px;align-items:center;} .filter-container label{font-weight:700;margin-right:5px}.filter-container select{padding:5px;border-radius:4px;border:1px solid #ccc} .results-container{flex-grow:1;overflow-y:auto;overflow-x:auto;border:1px solid #ddd} .ct-results-table{width:100%;border-collapse:collapse;font-size:12px}.ct-results-table td,.ct-results-table th{border:1px solid #ddd;padding:8px;text-align:left;vertical-align:middle}.ct-results-table th{background-color:#f2f2f2;font-weight:700;position:sticky;top:-1px} .ct-results-table tbody tr:not(.group-header) { cursor: pointer; } .ct-results-table tbody tr:not(.group-header):hover{background-color:#e9ecef}.ct-results-table tr.selected{background-color:#dbeafe!important;font-weight:700}.group-header td{background-color:#343a40;color:#fff;font-weight:700;padding:6px 8px} .actions-cell{text-align:left!important;white-space:nowrap}.action-btn{display:inline-block;padding:4px 8px;margin:0 2px;border-radius:4px;text-decoration:none;cursor:pointer;border:1px solid #ccc;font-size:11px}.action-btn:disabled{background-color:#e9ecef;color:#6c757d;cursor:not-allowed;border-color:#ddd}.action-btn.open-case{background-color:#6c757d;color:#fff}.action-btn.view-docs{background-color:#007bff;color:#fff;border-color:#007bff}.action-btn.view-users{background-color:#17a2b8;color:#fff;border-color:#17a2b8} .action-btn.download-doc { background-color: #28a745; color: white; border-color: #28a745; } .action-btn.review-refund-case { background-color: #6f42c1; color: white; border-color: #6f42c1; } .refresh-btn{margin-top:10px;padding:8px 12px;font-weight:700;cursor:pointer;border:1px solid #007bff;background-color:#007bff;color:#fff;border-radius:4px;transition:background-color .2s}.refresh-btn:hover{background-color:#0056b3} .group-header .group-title { font-weight: bold; font-size: 14px; color: white; } .group-header .group-subtitle { font-size: 11px; color: #ccc; margin-top: 2px; font-weight: normal; } .reported-cell { text-align: center !important; font-size: 16px; } .toggle-icon { display: inline-block; width: 1em; } .group-header.expanded .toggle-icon::before { content: '▼'; } .group-header.collapsed .toggle-icon::before { content: '►'; } .currency-wrapper { display: flex; justify-content: space-between; } .currency-num { font-variant-numeric: tabular-nums; } `); } 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 = `
No Case Selected
Please select a case from the "My Cases" tab

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.

Select a refund case and click "Refund Review" in the header or row.

`; // 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 --- 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 += ` ${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 += ` ${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 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) { // Get user names for this role type const userNames = caseUsersRoleMap[currentCaseRoleTypeCode] || []; let roleContent = `${currentCaseRoleTypeCode}`; if (userNames.length > 0) { roleContent += `
${userNames.map((name) => `• ${name}`).join("
")}
`; } roleCell.innerHTML = roleContent; } else { roleCell.innerHTML = `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", ), 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()); // Create role-to-users lookup map for Case History tab caseUsersRoleMap = {}; allCaseUsers.forEach((user) => { if (user.CaseRoleType && user.FullName) { if (!caseUsersRoleMap[user.CaseRoleType]) { caseUsersRoleMap[user.CaseRoleType] = []; } caseUsersRoleMap[user.CaseRoleType].push(user.FullName); } }); } catch (error) { 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); // Fetch user roles for matching with CaseRoleTypeCode await fetchCaseUsers(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)); 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); } // 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"); thead.innerHTML = `${headers.map((e) => `${e}`).join("")}`; table.appendChild(thead); return 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 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 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")); 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), ); } } 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(`tr.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), "tab-history" === tabId && selectedCaseId && selectedCaseId !== loadedHistoryForCaseId && fetchCaseHistory(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); } 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"); 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"; } }); } 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); }); } const casesFilterEl = document.getElementById("cases-status-filter"); if (casesFilterEl) casesFilterEl.addEventListener("change", renderMyCasesTable); const docsFilterEl = document.getElementById("docs-status-filter"); if (docsFilterEl) docsFilterEl.addEventListener("change", renderCaseDocumentsTable); const usersFilterEl = document.getElementById("users-role-filter"); 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); const refundDownloadBtn = document.getElementById("refund-download-btn"); if (refundDownloadBtn) refundDownloadBtn.addEventListener("click", downloadRefundExcel); 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), ); const toggleHistoryBtn = document.getElementById("toggle-history-btn"); if (toggleHistoryBtn) toggleHistoryBtn.addEventListener("click", (e) => toggleAllGroups("#tab-history .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(); } })();