Add 'coretabs.user.js'

This commit is contained in:
2025-08-19 02:10:34 +00:00
commit 5c8bce8319

290
coretabs.user.js Normal file
View File

@@ -0,0 +1,290 @@
// ==UserScript==
// @name CoreTabs
// @namespace https://git.diasbaskara.id
// @version 0.1
// @description Manage your cases easily.
// @author Dias Baskara
// @match https://coretax.intranet.pajak.go.id/*
// @grant GM_addStyle
// @run-at document-idle
// ==/UserScript==
(function() {
'use strict';
// --- SCRIPT CONFIGURATION ---
const AUTH_STORAGE_KEY = 'cats-angular-clientuser:https://coretax.intranet.pajak.go.id/identityprovider:cats-angular-client';
const DEFAULT_CASES_FILTER = 'In Progress';
// ----------------------------
// --- State Management ---
let allMyCases = [], allCaseDocuments = [], allCaseUsers = [];
let selectedCaseId = null;
let loadedDocsForCaseId = null;
let loadedUsersForCaseId = null;
function addStyles() {
GM_addStyle(`
#ct-sidebar{position:fixed;top:100px;right:-850px;width:850px;height:80vh;max-height:800px;background-color:#f9f9f9;border:1px solid #ccc;border-radius:8px 0 0 8px;box-shadow:-3px 0 8px rgba(0,0,0,.15);z-index:9999;transition:right .4s ease-in-out;display:flex;flex-direction:column;font-family:sans-serif}#ct-sidebar.open{right:0}#ct-sidebar-toggle{position:absolute;top:50%;right:850px;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-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}.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;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}.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:center!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; }
.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}
`);
}
function createSidebar() {
const sidebarContainer = document.createElement('div');
sidebarContainer.innerHTML = `
<div id="ct-sidebar">
<button id="ct-sidebar-toggle">API Fetcher</button>
<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>
</div>
<div id="ct-tab-content-area">
<div id="tab-my-cases" class="ct-tab-panel active"><div class="filter-container"><label for="cases-status-filter">Filter by Status:</label><select id="cases-status-filter"></select></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"><label for="docs-status-filter">Filter by Status:</label><select id="docs-status-filter"></select></div><div class="results-container"><p style="padding:15px;color:#666;">Please select a case or use 'View Docs'.</p></div></div>
<div id="tab-users" class="ct-tab-panel"><div class="filter-container"><label for="users-role-filter">Filter by Role:</label><select id="users-role-filter"></select></div><div class="results-container"><p style="padding:15px;color:#666;">Please select a case or use 'View Users'.</p></div></div>
</div>
</div>
`;
document.body.appendChild(sidebarContainer);
}
// --- RENDER FUNCTIONS ---
function renderMyCasesTable() {
const responseArea = document.querySelector('#tab-my-cases .results-container');
const filterValue = document.getElementById('cases-status-filter').value;
const filteredCases = filterValue === 'all' ? allMyCases : allMyCases.filter(c => c.CaseStatus === filterValue);
if (filteredCases.length === 0) {
responseArea.innerHTML = `<p style="padding:15px;color:#666;">No cases match the selected filter.</p>`;
return;
}
filteredCases.sort((a, b) => {
const typeCompare = (a.CaseTypeName || '').localeCompare(b.CaseTypeName || '');
if (typeCompare !== 0) return typeCompare;
return (b.CaseNumber || '').localeCompare(a.CaseNumber || '', undefined, { numeric: true });
});
const table = createTable(['Case Number', 'Taxpayer Name', 'Case Type', 'Status', 'Created Date', 'Actions']);
const tbody = document.createElement('tbody');
let currentGroup = '';
filteredCases.forEach(caseItem => {
if (caseItem.CaseTypeName !== currentGroup) {
currentGroup = caseItem.CaseTypeName;
tbody.innerHTML += `<tr class="group-header"><td colspan="6">${currentGroup || 'Uncategorized'}</td></tr>`;
}
const tr = document.createElement('tr');
const caseId = caseItem.AggregateIdentifier;
tr.dataset.id = caseId;
if (caseId === selectedCaseId) tr.classList.add('selected');
const createdDate = new Date(caseItem.CreatedDate).toLocaleDateString('id-ID');
const hasValidId = caseId && typeof caseId === 'string' && caseId.trim() !== '';
const disabledAttribute = hasValidId ? '' : 'disabled title="Action unavailable: Case ID is missing"';
tr.innerHTML = `
<td>${caseItem.CaseNumber || 'N/A'}</td>
<td>${caseItem.MainTaxpayerName || 'N/A'}</td>
<td>${caseItem.CaseTypeName || 'N/A'}</td>
<td>${caseItem.CaseStatus || 'N/A'}</td>
<td>${createdDate}</td>
<td class="actions-cell">
<a href="https://coretax.intranet.pajak.go.id/case-management/id-ID/case-overview/${caseId}" class="action-btn open-case" ${disabledAttribute}>Open</a>
<button class="action-btn view-docs" data-id="${caseId}" ${disabledAttribute}>View Docs</button>
<button class="action-btn view-users" data-id="${caseId}" ${disabledAttribute}>View Users</button>
</td>`;
tbody.appendChild(tr);
});
table.appendChild(tbody);
responseArea.innerHTML = '';
responseArea.appendChild(table);
tbody.addEventListener('click', handleCaseAction);
}
function renderCaseDocumentsTable() {
const responseArea = document.querySelector('#tab-docs .results-container');
const filterValue = document.getElementById('docs-status-filter').value;
const filteredDocs = filterValue === 'all' ? allCaseDocuments : allCaseDocuments.filter(d => d.DocumentStatus === filterValue);
if (filteredDocs.length === 0) {
responseArea.innerHTML = `<p style="padding:15px;color:#666;">No documents found or match the selected filter.</p>`;
return;
}
filteredDocs.sort((a, b) => (a.DocumentTypeCode || '').localeCompare(b.DocumentTypeCode));
// --- CHANGE 1 of 3: Updated the table headers ---
const table = createTable(['Letter Number', 'File Name', 'Status', 'Date', 'Actions']);
const tbody = document.createElement('tbody');
let currentGroup = '';
filteredDocs.forEach(doc => {
if (doc.DocumentTypeCode !== currentGroup) {
currentGroup = doc.DocumentTypeCode;
// --- CHANGE 2 of 3: Updated the colspan for the group header ---
tbody.innerHTML += `<tr class="group-header"><td colspan="5">${currentGroup || 'Uncategorized'}</td></tr>`;
}
const docDate = doc.DocumentDate ? new Date(doc.DocumentDate).toLocaleDateString('id-ID') : 'N/A';
// --- CHANGE 3 of 3: Changed the column data from DocumentTypeCode to FileName ---
tbody.innerHTML += `
<tr>
<td>${doc.LetterNumber || 'N/A'}</td>
<td>${doc.FileName || 'N/A'}</td>
<td>${doc.DocumentStatus || 'N/A'}</td>
<td>${docDate}</td>
<td class="actions-cell">
<button class="action-btn download-doc"
data-doc-id="${doc.DocumentAggregateIdentifier}"
data-filename="${doc.OriginalName}">
Download
</button>
</td>
</tr>`;
});
table.appendChild(tbody);
responseArea.innerHTML = '';
responseArea.appendChild(table);
tbody.addEventListener('click', handleDocumentAction);
}
function renderCaseUsersTable() {
const responseArea = document.querySelector('#tab-users .results-container');
const filterValue = document.getElementById('users-role-filter').value;
const filteredUsers = filterValue === 'all' ? allCaseUsers : allCaseUsers.filter(u => u.CaseRoleType === filterValue);
if (filteredUsers.length === 0) {
responseArea.innerHTML = `<p style="padding:15px;color:#666;">No users found or match the selected filter.</p>`;
return;
}
filteredUsers.sort((a, b) => (a.FullName || '').localeCompare(b.FullName || ''));
const table = createTable(['Full Name', 'NIP', 'Position', 'Office', 'Case Role']);
const tbody = document.createElement('tbody');
filteredUsers.forEach(user => {
tbody.innerHTML += `
<tr>
<td>${user.FullName || 'N/A'}</td>
<td>${user.Nip || 'N/A'}</td>
<td>${user.Jabatan || 'N/A'}</td>
<td>${user.OfficeName || 'N/A'}</td>
<td>${user.CaseRoleType || 'N/A'}</td>
</tr>`;
});
table.appendChild(tbody);
responseArea.innerHTML = '';
responseArea.appendChild(table);
}
// --- 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 authToken=getAuthToken(),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 authToken=getAuthToken(),apiUrl="https://coretax.intranet.pajak.go.id/casemanagement/api/caseuser/list",payload={AggregateIdentifier:caseId,First:0,Rows:200,SortField:"",SortOrder:1,Filters:[],LanguageId:"id-ID"},fetchOptions={method:"POST",headers:getHeaders(caseId),body:JSON.stringify(payload)},response=await fetch(apiUrl,fetchOptions);if(!response.ok){const errorData=await response.json();throw new Error(`API Error: ${errorData.Message||response.statusText}`)}const data=await response.json();allCaseUsers=data?.Payload?.Data||[],loadedUsersForCaseId=caseId,populateFilter("users-role-filter",allCaseUsers,"CaseRoleType"),renderCaseUsersTable()}catch(error){handleError(error,responseArea)}}
async function downloadDocument(docId, filename, button) {
const originalText = button.textContent;
button.textContent = 'Downloading...';
button.disabled = true;
try {
const authToken = getAuthToken();
const apiUrl = 'https://coretax.intranet.pajak.go.id/documentmanagement/api/download';
const payload = { DocumentAggregateIdentifier: docId, IsDocumentCases: false, IsNeedWatermark: null };
const fetchOptions = {
method: 'POST',
headers: getHeaders(), // Using generic headers should be fine
body: JSON.stringify(payload)
};
const 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();
const url = window.URL.createObjectURL(blob);
const 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 = false;
}
}
// --- 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 getAuthToken(){const userDataString=localStorage.getItem(AUTH_STORAGE_KEY),userData=userDataString?JSON.parse(userDataString):null,authToken=userData?.access_token;if(!authToken)throw new Error("Authorization Token not found.");return authToken}
function createTable(headers){const table=document.createElement("table");table.className="ct-results-table";const thead=document.createElement("thead");return thead.innerHTML=`<tr>${headers.map(e=>`<th>${e}</th>`).join("")}</tr>`,table.appendChild(thead),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 handleCaseAction(event) {
const selectedRow = event.target.closest('tr');
if (!selectedRow || selectedRow.classList.contains('group-header')) return;
const caseId = selectedRow.dataset.id;
if (!caseId) return;
selectedCaseId = caseId;
loadedDocsForCaseId = null;
loadedUsersForCaseId = null;
const allRows = selectedRow.closest('tbody').querySelectorAll('tr');
allRows.forEach(row => row.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');
}
}
function handleDocumentAction(event) {
const downloadButton = event.target.closest('.download-doc');
if (downloadButton) {
const docId = downloadButton.dataset.docId;
const filename = downloadButton.dataset.filename;
downloadDocument(docId, filename, downloadButton);
}
}
function switchTab(tabId) {
document.querySelectorAll('.ct-tab-button').forEach(btn => btn.classList.remove('active'));
document.querySelectorAll('.ct-tab-panel').forEach(panel => panel.classList.remove('active'));
document.querySelector(`[data-tab="${tabId}"]`).classList.add('active');
document.getElementById(tabId).classList.add('active');
if (tabId === 'tab-docs') {
if (selectedCaseId && selectedCaseId !== loadedDocsForCaseId) {
fetchCaseDocuments(selectedCaseId);
}
} else if (tabId === 'tab-users') {
if (selectedCaseId && selectedCaseId !== loadedUsersForCaseId) {
fetchCaseUsers(selectedCaseId);
}
}
}
// --- MAIN INITIALIZATION ---
function main() {
addStyles();
createSidebar();
document.getElementById('ct-sidebar-toggle').addEventListener('click', () => {
document.getElementById('ct-sidebar').classList.toggle('open');
});
document.getElementById('ct-tab-bar').addEventListener('click', (e) => {
if (e.target.matches('.ct-tab-button')) switchTab(e.target.dataset.tab);
});
document.getElementById('cases-status-filter').addEventListener('change', renderMyCasesTable);
document.getElementById('docs-status-filter').addEventListener('change', renderCaseDocumentsTable);
document.getElementById('users-role-filter').addEventListener('change', renderCaseUsersTable);
fetchMyCases();
}
main();
})();