From 5c8bce83190c80b01cf9a95c6ac8a6038288d5dd Mon Sep 17 00:00:00 2001 From: Dias Baskara Date: Tue, 19 Aug 2025 02:10:34 +0000 Subject: [PATCH] Add 'coretabs.user.js' --- coretabs.user.js | 290 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 290 insertions(+) create mode 100644 coretabs.user.js diff --git a/coretabs.user.js b/coretabs.user.js new file mode 100644 index 0000000..0504748 --- /dev/null +++ b/coretabs.user.js @@ -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 = ` +
+ +
+ + + +
+
+

Loading my cases...

+

Please select a case or use 'View Docs'.

+

Please select a case or use 'View Users'.

+
+
+ `; + 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 = `

No cases match the selected filter.

`; + 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 += `${currentGroup || 'Uncategorized'}`; + } + 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 = ` + ${caseItem.CaseNumber || 'N/A'} + ${caseItem.MainTaxpayerName || 'N/A'} + ${caseItem.CaseTypeName || 'N/A'} + ${caseItem.CaseStatus || 'N/A'} + ${createdDate} + + Open + + + `; + 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 = `

No documents found or match the selected filter.

`; + 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 += `${currentGroup || 'Uncategorized'}`; + } + 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 += ` + + ${doc.LetterNumber || 'N/A'} + ${doc.FileName || 'N/A'} + ${doc.DocumentStatus || 'N/A'} + ${docDate} + + + + `; + }); + 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 = `

No users found or match the selected filter.

`; + 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 += ` + + ${user.FullName || 'N/A'} + ${user.Nip || 'N/A'} + ${user.Jabatan || 'N/A'} + ${user.OfficeName || 'N/A'} + ${user.CaseRoleType || 'N/A'} + `; + }); + 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='

Loading documents...

';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='

Loading users...

';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='',values.sort().forEach(e=>{select.innerHTML+=``})} + function getAuthToken(){const userDataString=localStorage.getItem(AUTH_STORAGE_KEY),userData=userDataString?JSON.parse(userDataString):null,authToken=userData?.access_token;if(!authToken)throw new Error("Authorization Token not found.");return authToken} + function createTable(headers){const table=document.createElement("table");table.className="ct-results-table";const thead=document.createElement("thead");return thead.innerHTML=`${headers.map(e=>`${e}`).join("")}`,table.appendChild(thead),table} + function handleError(error,area){console.error("Userscript Error:",error);let errorHtml;error.message.includes("Authorization Token not found")?errorHtml=` +
+ Session Expired or Token Not Found +

Please refresh the page to log in again.

+ +
`:errorHtml=`

An error occurred:
${error.message}

`,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(); +})(); \ No newline at end of file