diff --git a/coretabs.user.js b/coretabs.user.js index bd9b6b0..c05e3b0 100644 --- a/coretabs.user.js +++ b/coretabs.user.js @@ -1,7 +1,7 @@ // ==UserScript== -// @name CoreTabs +// @name CoreTabs1 // @namespace https://git.diasbaskara.id/diasbaskara/userscripts/ -// @version 0.4 +// @version 0.5 // @description Manage your cases easily. // @author Dias Baskara // @match https://coretax.intranet.pajak.go.id/* @@ -10,25 +10,58 @@ // @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 = [], + 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; } @@ -56,13 +89,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 = `
Loading my cases...
Please select a case to view its documents.
Please select a case to view its users.
Please select a case to view its history.
No 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+=`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 += `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+=`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 += `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 += `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=` +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 += `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 = ` -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); +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()); - // --- 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=`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 = `Please refresh the page to log in again.
-An error occurred:
${error.message}
An error occurred:
${error.message}
Step 1/3: Fetching Sub Process ID...
`; + if (!caseId) throw new Error("A case must be selected."); + const subProcessId = await fetchSubProcessId(caseId); + responseArea.innerHTML = `Step 2/3: Fetching reference number (trying primary API)...
`; + const referenceNumber = await fetchC02FormDetail(caseId, subProcessId); + responseArea.innerHTML = `Step 3/3: Fetching refund details...
`; + await fetchRefundReview(caseId, referenceNumber); + } catch (error) { + handleError(error, responseArea); + } finally { + button.textContent = originalText; + button.disabled = false; + } + } + + function handleCaseSelection(event) { + const selectedRow = event.target.closest("tr"); + if (!selectedRow || selectedRow.classList.contains("group-header")) return; + const caseId = selectedRow.dataset.id; + if (!caseId || caseId === selectedCaseId) return; + + const selectedCase = allMyCases.find( + (e) => e.AggregateIdentifier === caseId, + ); + updateHeader(selectedCase); + selectedCaseId = caseId; + loadedDocsForCaseId = null; + loadedUsersForCaseId = null; + refundReviewData = []; + filteredRefundData = []; + document.querySelector("#tab-refund .results-container").innerHTML = + `Please use the 'Review Refund' action on a relevant case.
`; + document.getElementById("refund-download-btn").disabled = true; + + const allRows = selectedRow.closest("tbody").querySelectorAll("tr"); + allRows.forEach((e) => e.classList.remove("selected")); + selectedRow.classList.add("selected"); + + const actionButton = event.target.closest(".action-btn"); + if (actionButton) { + if (actionButton.matches(".view-docs")) { + switchTab("tab-docs"); + } else if (actionButton.matches(".view-users")) { + switchTab("tab-users"); + } else if (actionButton.matches(".review-refund-case")) { + startRefundReviewProcess(caseId, actionButton); + } + } + } + function handleDocumentAction(event) { + const target = event.target.closest(".action-btn"); + if (!target) return; + if (target.matches(".download-doc")) { + const docId = target.dataset.docId; + const filename = target.dataset.filename; + downloadDocument(docId, filename, target); + } + } + function handleGroupToggle(event) { + const headerRow = event.target.closest(".group-header"); + if (!headerRow) return; + const groupId = headerRow.dataset.groupId; + if (!groupId) return; + headerRow.classList.toggle("collapsed"); + headerRow.classList.toggle("expanded"); + const isCollapsed = headerRow.classList.contains("collapsed"); + const tableBody = headerRow.parentElement; + const memberRows = tableBody.querySelectorAll(`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 { - titleEl.textContent = 'No Case Selected'; - subtitleEl.textContent = 'Please select a case from the "My Cases" tab'; + row[map.excelHeader] = + item[map.apiField] === null ? "" : item[map.apiField]; } - } - - // 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; + }); + 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"; } + }); } - 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); } + // 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"; } - } - 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); + }); } - 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'; + 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); + }); } - // --- 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 casesFilterEl = document.getElementById("cases-status-filter"); + if (casesFilterEl) + casesFilterEl.addEventListener("change", renderMyCasesTable); - 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 docsFilterEl = document.getElementById("docs-status-filter"); + if (docsFilterEl) + docsFilterEl.addEventListener("change", renderCaseDocumentsTable); - fetchMyCases(); - } + 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(); -})(); \ No newline at end of file + } +})();