Files
userscripts/coretabs.user.js
2025-11-27 01:37:39 +00:00

1275 lines
53 KiB
JavaScript

// ==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 = `
<div id="ct-sidebar">
<button id="ct-sidebar-toggle">Coretabs</button>
<div id="ct-header-area">
<div id="ct-header-icon">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M20 6h-8l-2-2H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V8c0-1.1-.9-2-2-2zm0 12H4V6h5.17l2 2H20v10z"/></svg>
</div>
<div id="ct-header-text">
<div id="ct-header-title">No Case Selected</div>
<div id="ct-header-subtitle">Please select a case from the "My Cases" tab</div>
</div>
<div id="ct-header-actions"></div>
</div>
<div id="ct-tab-bar">
<button class="ct-tab-button active" data-tab="tab-my-cases">My Cases</button>
<button class="ct-tab-button" data-tab="tab-docs">Case Documents</button>
<button class="ct-tab-button" data-tab="tab-users">Case Users</button>
<button class="ct-tab-button" data-tab="tab-history">Case History</button>
<button class="ct-tab-button" data-tab="tab-refund">Refund Review</button>
</div>
<div id="ct-tab-content-area">
<div id="tab-my-cases" class="ct-tab-panel active"><div class="filter-container"><div><label for="cases-status-filter">Filter by Status:</label><select id="cases-status-filter"></select></div><button id="toggle-cases-btn" class="action-btn" style="margin-left:auto">Collapse All</button></div><div class="results-container"><p style="padding:15px;color:#666;">Loading my cases...</p></div></div>
<div id="tab-docs" class="ct-tab-panel"><div class="filter-container"><div><label for="docs-status-filter">Filter by Status:</label><select id="docs-status-filter"></select></div><button id="toggle-docs-btn" class="action-btn" style="margin-left:auto">Collapse All</button></div><div class="results-container"><p style="padding:15px;color:#666;">Please select a case to view its documents.</p></div></div>
<div id="tab-users" class="ct-tab-panel"><div class="filter-container"><div><label for="users-role-filter">Filter by Role:</label><select id="users-role-filter"></select></div></div><div class="results-container"><p style="padding:15px;color:#666;">Please select a case to view its users.</p></div></div>
<div id="tab-history" class="ct-tab-panel">
<div class="filter-container">
<div><label for="history-type-filter">Filter by Case Type:</label><select id="history-type-filter"></select></div>
<button id="toggle-history-btn" class="action-btn" style="margin-left:auto">Collapse All</button>
</div>
<div class="results-container"><p style="padding:15px;color:#666;">Please select a case to view its history.</p></div>
</div>
<div id="tab-refund" class="ct-tab-panel">
<div class="filter-container">
<div><label for="refund-reported-filter">Filter by Reported:</label><select id="refund-reported-filter"></select></div>
<button id="toggle-refund-btn" class="action-btn" style="margin-left:auto">Collapse All</button>
<button id="refund-download-btn" class="action-btn download-doc">Download Excel</button>
</div>
<div class="results-container"><p style="padding:15px;color:#666;">Select a refund case and click "Refund Review" in the header or row.</p></div>
</div>
</div>
</div>
`;
// 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 =
'<p style="padding:15px;color:#666;">No cases match the selected filter.</p>');
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 += `<tr class="group-header expanded" data-group-id="my-cases-group-${groupIndex}"><td colspan="6"><span class="toggle-icon"></span>${currentGroup || "Uncategorized"}</td></tr>`;
}
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 = `<button class="action-btn review-refund-case" data-id="${o}">Refund Review</button>`),
(t.innerHTML = `
<td>${e.CaseNumber || "N/A"}</td>
<td>${e.MainTaxpayerName || "N/A"}</td>
<td>${e.CaseTypeName || "N/A"}</td>
<td>${e.CaseStatus || "N/A"}</td>
<td>${a}</td>
<td class="actions-cell">
<a href="https://coretax.intranet.pajak.go.id/case-management/id-ID/case-overview/${o}" class="action-btn open-case" ${r}>Open</a>
<button class="action-btn view-docs" data-id="${o}" ${r}>View Docs</button>
<button class="action-btn view-users" data-id="${o}" ${r}>View Users</button>
${d}
</td>`),
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 =
'<p style="padding:15px;color:#666;">No documents found or match the selected filter.</p>');
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 += `<tr class="group-header expanded" data-group-id="docs-group-${groupIndex}"><td colspan="5"><span class="toggle-icon"></span>${currentGroup || "Uncategorized"}</td></tr>`;
}
const t = e.DocumentDate
? new Date(e.DocumentDate).toLocaleDateString("id-ID")
: "N/A";
tbody.innerHTML += `
<tr class="group-member docs-group-${groupIndex}">
<td>${e.LetterNumber || "N/A"}</td>
<td>${e.FileName || "N/A"}</td>
<td>${e.DocumentStatus || "N/A"}</td>
<td>${t}</td>
<td class="actions-cell">
<button class="action-btn download-doc" data-doc-id="${e.DocumentAggregateIdentifier}" data-filename="${e.OriginalName}">Download</button>
</td>
</tr>`;
}),
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 =
'<p style="padding:15px;color:#666;">No users found or match the selected filter.</p>');
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 += `
<tr>
<td>${e.FullName || "N/A"}</td>
<td>${e.Nip || "N/A"}</td>
<td>${e.Jabatan || "N/A"}</td>
<td>${e.OfficeName || "N/A"}</td>
<td>${e.CaseRoleType || "N/A"}</td>
</tr>`;
}),
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 =
'<p style="padding:15px;color:#666;">No history data available for this case.</p>';
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 =
'<p style="padding:15px;color:#666;">No history items match the selected filter.</p>';
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 += `<tr class="group-header expanded" data-group-id="history-group-${sanitizedCaseType}"><td colspan="3"><span class="toggle-icon"></span>${caseType} (${groupItems.length} items)</td></tr>`;
// 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 = `<strong style="color: #333;">${currentCaseRoleTypeCode}</strong>`;
if (userNames.length > 0) {
roleContent += `<br><small style="color: #888; font-size: 10px;">${userNames.map((name) => `${name}`).join("<br>")}</small>`;
}
roleCell.innerHTML = roleContent;
} else {
roleCell.innerHTML = `<strong style="color: #666; font-style: italic;">Not available</strong>`;
}
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 =
'<p style="padding:15px;color:#666;">No refund review data matches the filter.</p>');
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 = `
<span class="toggle-icon"></span>
<span>
<div class="group-title">${e.Name || "Unknown Name"}</div>
<div class="group-subtitle">${e.Tin || "Unknown TIN"}</div>
</span>`));
}
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
? '<span style="color: #28a745;">✔</span>'
: '<span style="color: #dc3545;">❌</span>';
((o.insertCell().textContent = e.DocumentNumber || "N/A"),
(o.insertCell().textContent = a));
const r = o.insertCell();
r.innerHTML = `<div class="currency-wrapper"><span>Rp</span><span class="currency-num">${(e.SellingPrice || 0).toLocaleString("id-ID")}</span></div>`;
const d = o.insertCell();
d.innerHTML = `<div class="currency-wrapper"><span>Rp</span><span class="currency-num">${(e.VatPaid || 0).toLocaleString("id-ID")}</span></div>`;
const n = o.insertCell();
((n.innerHTML = `<div class="currency-wrapper"><span>Rp</span><span class="currency-num">${(e.StlgPaid || 0).toLocaleString("id-ID")}</span></div>`),
(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 =
'<p style="padding:15px;color:#666;">Loading documents...</p>';
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 =
'<p style="padding:15px;color:#666;">Loading users...</p>';
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 =
'<p style="padding:15px;color:#666;">Loading history...</p>';
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 = `<p style="padding:15px;color:#666;">Step 2/3: Fetching reference number (trying fallback API)...</p>`;
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 = '<option value="all">Show All</option>'),
values.sort().forEach((e) => {
select.innerHTML += `<option value="${e}">${e}</option>`;
}));
}
function populateBooleanFilter(selectId) {
const e = document.getElementById(selectId);
e &&
(e.innerHTML =
'<option value="all">Show All</option><option value="true">Yes</option><option value="false">No</option>');
}
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 = `<tr>${headers.map((e) => `<th>${e}</th>`).join("")}</tr>`;
table.appendChild(thead);
return table;
}
function handleError(error, area) {
console.error("Userscript Error:", error);
let errorHtml;
(error.message.includes("Authorization Token not found")
? (errorHtml = `
<div style="padding:15px; color: #d9534f; text-align: center;">
<b>Session Expired or Token Not Found</b>
<p style="margin: 10px 0;">Please refresh the page to log in again.</p>
<button id="auth-refresh-btn" class="refresh-btn">Refresh Page</button>
</div>`)
: (errorHtml = `<p style="color: #d9534f;padding:15px;"><b>An error occurred:</b><br>${error.message}</p>`),
(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 = `<p style="padding:15px;color:#666;">Step 1/3: Fetching Sub Process ID...</p>`;
if (!caseId) throw new Error("A case must be selected.");
const subProcessId = await fetchSubProcessId(caseId);
responseArea.innerHTML = `<p style="padding:15px;color:#666;">Step 2/3: Fetching reference number (trying primary API)...</p>`;
const referenceNumber = await fetchC02FormDetail(caseId, subProcessId);
responseArea.innerHTML = `<p style="padding:15px;color:#666;">Step 3/3: Fetching refund details...</p>`;
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 =
`<p style="padding:15px;color:#666;">Please use the 'Review Refund' action on a relevant case.</p>`;
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();
}
})();