Add Case History tab with grouped history view

Introduces a new 'Case History' tab to display case routing history, grouped by case type and including current role information. Implements API calls to fetch history and subprocess data, adds filtering and group toggling, and updates tab switching logic to support the new feature.
This commit is contained in:
Dias Baskara
2025-11-26 13:02:55 +07:00
parent b43fae30b1
commit 50401a73c9

View File

@@ -48,11 +48,14 @@
let allMyCases = [],
allCaseDocuments = [],
allCaseUsers = [],
refundReviewData = [];
let filteredRefundData = [];
let selectedCaseId = null;
let loadedDocsForCaseId = null;
let loadedUsersForCaseId = null;
caseHistoryData = [],
caseSubProcessData = {},
refundReviewData = [],
filteredRefundData = [],
selectedCaseId = null,
loadedDocsForCaseId = null,
loadedUsersForCaseId = null,
loadedHistoryForCaseId = null;
function addStyles() {
GM_addStyle(`
@@ -109,12 +112,20 @@
<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>
@@ -298,6 +309,118 @@
(responseArea.innerHTML = ""),
responseArea.appendChild(table));
}
function renderCaseHistoryTable() {
const responseArea = document.querySelector(
"#tab-history .results-container",
);
if (!caseHistoryData || caseHistoryData.length === 0) {
responseArea.innerHTML =
'<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) {
roleCell.innerHTML = `<strong>Current Role:</strong><br><small style="color: #666;">${currentCaseRoleTypeCode}</small>`;
} else {
roleCell.innerHTML = `<strong>Current Role:</strong><br><small style="color: #666; font-style: italic;">Not available</small>`;
}
currentRoleRow.appendChild(roleCell);
// Last To Step in Workflow Step column
const workflowStepCell = document.createElement("td");
workflowStepCell.textContent = mostRecentItem.ToWorkflowStep || "-";
currentRoleRow.appendChild(workflowStepCell);
tbody.appendChild(currentRoleRow);
// Create member rows for this group
groupItems.forEach((item) => {
const row = document.createElement("tr");
row.className = `group-member history-group-${sanitizedCaseType}`;
// Format RoutingDate
const routingDate = new Date(item.RoutingDate);
const dateCell = document.createElement("td");
dateCell.textContent = routingDate.toLocaleString("id-ID");
row.appendChild(dateCell);
// PerformedByUser
const performedByCell = document.createElement("td");
performedByCell.textContent = item.PerformedByUser || "-";
row.appendChild(performedByCell);
// FromWorkflowStep (renamed to Workflow Step)
const workflowStepCell = document.createElement("td");
workflowStepCell.textContent = item.FromWorkflowStep || "-";
row.appendChild(workflowStepCell);
tbody.appendChild(row);
});
});
responseArea.innerHTML = "";
table.appendChild(tbody);
responseArea.appendChild(table);
tbody.addEventListener("click", handleGroupToggle);
}
function renderRefundReviewTable() {
const responseArea = document.querySelector(
"#tab-refund .results-container",
@@ -481,6 +604,91 @@
handleError(error, responseArea);
}
}
async function fetchCaseHistory(caseId) {
const responseArea = document.querySelector(
"#tab-history .results-container",
);
responseArea.innerHTML =
'<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);
populateFilter("history-type-filter", caseHistoryData, "CaseType");
renderCaseHistoryTable();
} catch (error) {
handleError(error, responseArea);
}
}
async function fetchCaseSubProcessData(caseId) {
try {
const apiUrl =
"https://coretax.intranet.pajak.go.id/casemanagement/api/casesubprocess/list",
payload = {
First: 0,
Rows: 10000,
Filters: [],
AggregateIdentifier: caseId,
},
fetchOptions = {
method: "POST",
headers: getHeaders(caseId),
body: JSON.stringify(payload),
},
response = await fetch(apiUrl, fetchOptions);
if (!response.ok) {
const errorData = await response.json();
throw new Error(
`Case SubProcess API Error: ${errorData.Message || response.statusText}`,
);
}
const data = await response.json();
const subProcessData = data?.Payload?.Data || [];
// Create a lookup map for WorkflowStepIdentifier to CaseRoleTypeCode
caseSubProcessData = {};
subProcessData.forEach((item) => {
if (item.WorkflowStepIdentifier && item.CaseRoleTypeCode) {
caseSubProcessData[item.WorkflowStepIdentifier] =
item.CaseRoleTypeCode;
}
});
} catch (error) {
console.error("Error fetching case subprocess data:", error);
caseSubProcessData = {};
}
}
async function downloadDocument(docId, filename, button) {
const originalText = button.textContent;
((button.textContent = "Downloading..."), (button.disabled = !0));
@@ -673,11 +881,9 @@
const table = document.createElement("table");
table.className = "ct-results-table";
const thead = document.createElement("thead");
return (
(thead.innerHTML = `<tr>${headers.map((e) => `<th>${e}</th>`).join("")}</tr>`),
table.appendChild(thead),
table
);
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);
@@ -823,7 +1029,7 @@
headerRow.classList.toggle("expanded");
const isCollapsed = headerRow.classList.contains("collapsed");
const tableBody = headerRow.parentElement;
const memberRows = tableBody.querySelectorAll(`.group-member.${groupId}`);
const memberRows = tableBody.querySelectorAll(`tr.group-member.${groupId}`);
memberRows.forEach((row) => {
row.style.display = isCollapsed ? "none" : "table-row";
});
@@ -837,14 +1043,18 @@
.forEach((e) => e.classList.remove("active")),
document.querySelector(`[data-tab="${tabId}"]`).classList.add("active"),
document.getElementById(tabId).classList.add("active"),
"tab-docs" === tabId
? selectedCaseId &&
selectedCaseId !== loadedDocsForCaseId &&
fetchCaseDocuments(selectedCaseId)
: "tab-users" === tabId &&
selectedCaseId &&
selectedCaseId !== loadedUsersForCaseId &&
fetchCaseUsers(selectedCaseId));
"tab-docs" === tabId &&
selectedCaseId &&
selectedCaseId !== loadedDocsForCaseId &&
fetchCaseDocuments(selectedCaseId),
"tab-users" === tabId &&
selectedCaseId &&
selectedCaseId !== loadedUsersForCaseId &&
fetchCaseUsers(selectedCaseId),
"tab-history" === tabId &&
selectedCaseId &&
selectedCaseId !== loadedHistoryForCaseId &&
fetchCaseHistory(selectedCaseId));
}
function downloadRefundExcel() {
if (!filteredRefundData || filteredRefundData.length === 0) {
@@ -992,6 +1202,10 @@
if (usersFilterEl)
usersFilterEl.addEventListener("change", renderCaseUsersTable);
const historyFilterEl = document.getElementById("history-type-filter");
if (historyFilterEl)
historyFilterEl.addEventListener("change", renderCaseHistoryTable);
const refundFilterEl = document.getElementById("refund-reported-filter");
if (refundFilterEl)
refundFilterEl.addEventListener("change", renderRefundReviewTable);
@@ -1018,6 +1232,12 @@
toggleAllGroups("#tab-refund .results-container", e.target),
);
const toggleHistoryBtn = document.getElementById("toggle-history-btn");
if (toggleHistoryBtn)
toggleHistoryBtn.addEventListener("click", (e) =>
toggleAllGroups("#tab-history .results-container", e.target),
);
// initial load
fetchMyCases();
}