1275 lines
53 KiB
JavaScript
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();
|
|
}
|
|
})();
|