Files
arch-panel-extension-browser/content-script.js

11404 lines
340 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
(() => {
const TARGET_ORIGIN = "https://spa.oracle.com";
const TARGET_PATHNAME =
"/oalcrm/web/api/g2m-consumer-application/ui/index.html";
const TARGET_PARAM = "ojr";
const TARGET_PARAM_VALUE = "workload_workbench";
const TARGET_APP_URL =
"https://spa.oracle.com/oalcrm/web/api/g2m-consumer-application/ui/index.html?ojr=workload_workbench";
const HCM_ORIGIN = "https://eeho.fa.us2.oraclecloud.com";
const HCM_WELCOME_PATHNAME = "/hcmUI/faces/FuseWelcome";
const HCM_MY_INFORMATION_APPS_GROUP_ID = "yourapps_groupNode_my_information";
const WORKLIST_ORIGIN = "https://worklist.oracle.com";
const WORKLIST_SAASUI_PATHNAME = "/oalapp/pub/worklist/saasui/index.html";
const USER_BUTTON_LABEL = "[[displayUser().fullName]]";
const ALLOWED_STAGE_TYPES = ["SQL", "Pipeline", "Upside", "Forecast"];
const SELECTORS = {
header: ".oj-oal-ux-global-header.oj-oal-ux-global-header-pillar-cx",
functions: ".oj-flex.oj-oal-ux-global-header-functions",
};
const IDS = {
mount: "arch-panel-extension-mount",
button: "arch-panel-extension-button",
style: "arch-panel-extension-style",
overlay: "arch-panel-extension-overlay",
dialog: "arch-panel-extension-dialog",
hcmTile: "arch-panel-extension-hcm-tile",
spaFrame: "arch-panel-extension-spa-frame",
spaFrameHost: "arch-panel-extension-spa-frame-host",
};
const CLASSES = {
modalOpen: "arch-panel-extension-modal-open",
overlayOpen: "is-open",
};
const STORAGE_KEYS = {
theme: "arch-panel-extension-theme",
sidebarCollapsed: "arch-panel-extension-sidebar-collapsed",
calendarView: "arch-panel-extension-calendar-view",
};
const CACHE = {
dbName: "arch-panel-extension-db",
dbVersion: 1,
storeName: "datasets",
datasetKey: "workload-dashboard",
taskTypeKey: "time-management-task-types",
worklistTokenKey: "worklist-saasui-token",
};
const DETAIL_SORT_KEYS = {
opportunityNumber: "opportunityNumber",
customerName: "customerName",
workload: "workload",
rampMonths: "rampMonths",
adjustedACR: "adjustedACR",
opportunityForecastTypeGroup: "opportunityForecastTypeGroup",
hasSR: "hasSR",
hasAction: "hasAction",
};
const BRIDGE = {
source: "arch-panel-extension",
fetchType: "ARCH_PANEL_FETCH",
resultType: "ARCH_PANEL_FETCH_RESULT",
headersType: "ARCH_PANEL_HEADERS",
headersResultType: "ARCH_PANEL_HEADERS_RESULT",
workbenchAuthHeadersType: "ARCH_PANEL_WORKBENCH_AUTH_HEADERS",
readyType: "ARCH_PANEL_BRIDGE_READY",
scriptId: "arch-panel-extension-page-bridge",
};
const API_ENDPOINTS = {
currentUser:
"/oalcrm/web/api/identity-management/users/current",
worklistCurrentUser:
"https://worklist.oracle.com/oalapp/pub/wlp/svc/user-service/user",
customerSummary:
"/oalcrm/web/api/provider-proxy/wwb-provider/service/elastic/customers/listSummary",
customerWorkloads:
"/oalcrm/web/api/provider-proxy/wwb-provider/service/workbench-proxy/wwb/mgmt/customers",
workloadActions:
"/oalcrm/web/api/provider-proxy/wwb-provider/service/workbench-proxy/wwb/mgmt/actions",
workloadServiceRequests:
"/oalcrm/web/api/provider-proxy/wwb-provider/service/workbench-proxy/wwb/mgmt/workloads",
};
const COMCIP_REQUEST = {
url:
"https://comcipapic-oalprod.integration.ocp.oraclecloud.com/ic/builder/rt/oalset_semc/live;profile=PROD/services/auth/1.1/proxy/oalsetCRMRestAPIElastic/uri/https/eeho.fa.us2.oraclecloud.com/crmRestApi/searchResources/latest/custom-actions/queries",
timeEntriesSummaryUrl:
"https://comcipapic-oalprod.integration.ocp.oraclecloud.com/ic/builder/rt/oalset_semc/live;profile=PROD/services/auth/1.1/proxy/oalsetSeaaSOKECustomRestAPI/uri/https/gxpap.oracle.com/oalcrm/service/set/seaas/mgmt/timeEntries/summary",
resourceCurrentUserUrl:
"https://comcipapic-oalprod.integration.ocp.oraclecloud.com/ic/builder/rt/oalset_timeentrymobile/live;profile=PROD/services/auth/1.1/proxy/oke_seaas/uri/https/gxpap.oracle.com/oalcrm/service/set/seaas/crm/resourceUsers",
timeEntryWeekDetailsUrl:
"https://comcipapic-oalprod.integration.ocp.oraclecloud.com/ic/builder/rt/oalset_timeentrymobile/live;profile=PROD/services/auth/1.1/proxy/oke_seaas/uri/https/gxpap.oracle.com/oalcrm/service/set/seaas/mgmt/timeEntries/weekDetails",
activeServiceRequestsUrl:
"https://comcipapic-oalprod.integration.ocp.oraclecloud.com/ic/builder/rt/oalset_timeentrymobile/live;profile=PROD/services/auth/1.1/proxy/oke_seaas/uri/https/gxpap.oracle.com/oalcrm/service/set/seaas/mgmt/timeEntries/activeServiceRequests",
timeEntriesUrl:
"https://comcipapic-oalprod.integration.ocp.oraclecloud.com/ic/builder/rt/oalset_timeentrymobile/live;profile=PROD/services/auth/1.1/proxy/oke_seaas/uri/https/gxpap.oracle.com/oalcrm/service/set/seaas/mgmt/timeEntries",
activityServiceRequestsUrl:
"https://comcipapic-oalprod.integration.ocp.oraclecloud.com/ic/builder/rt/oalset_timeentrymobile/live;profile=PROD/services/auth/1.1/proxy/crmRestApiSearchResourcesLatestCustomActions/uri/https/eeho.fa.us2.oraclecloud.com/crmRestApi/resources/latest/serviceRequests",
taskTypeUrl:
"https://comcipapic-oalprod.integration.ocp.oraclecloud.com/ic/builder/rt/oalset_timeentrymobile/live;profile=PROD/services/auth/1.1/proxy/oke_seaas/uri/https/gxpap.oracle.com/oalcrm/service/set/seaas/crm/lookups?lookupType=SCTA_TASK_TYPE&type=CUSTOM",
timeEntryAppUrl:
"https://comcipapic-oalprod.integration.ocp.oraclecloud.com/ic/builder/rt/oalset_timeentrymobile/live/webApps/timeentry/",
appVersion: "version_1754044416761",
};
const COMCIP_TIME_ENTRY_FRAME_ID = "arch-panel-extension-comcip-timeentry-frame";
const ACTIVITY_REQUEST_REFERENCE_SR_NUMBERS = [
"SR0001440593",
"SR0001440475",
"SR0001434642",
"SR0001434671",
"SR0001387564",
"SR0001439267",
"SR0001411935",
"SR0001428065",
"SR0001433200",
"SR0001433217",
"SR0001433030",
"SR0001430407",
"SR0001432386",
"SR0001431923",
"SR0001418256",
"SR0001398520",
"SR0001377462",
"SR0001367044",
"SR0001419998",
"SR0001399117",
"SR0001413684",
"SR0001422541",
"SR0001415397",
"SR0001418535",
"SR0001421040",
"SR0001421074",
"SR0001388866",
"SR0001335148",
"SR0001416756",
"SR0001380511",
"SR0001413965",
"SR0001413598",
"SR0001409121",
"SR0001404828",
"SR0001404549",
"SR0001402376",
"SR0001399121",
"SR0001294405",
"SR0001308472",
];
const ACTION_TEAM_OPTIONS = [
{
value: "DM_AUTONOMOUS",
label: "Data Platform - Autonomous Database",
},
{
value: "CI_COMPUTE",
label: "Cloud Infrastructure - Compute including HPC",
},
{
value: "APP_DEV_AI_CLOUD_SERVICES",
label: "App Dev - AI Cloud Services",
},
{
value: "DEV_CLOUD_NATIVE_APP",
label: "App Dev - Cloud Native",
},
{
value: "CI_BIG_COMPUTE_HPC",
label: "Cloud Infrastructure - AI Infra/GPU",
},
{
value: "CI_MULTI_CLOUD",
label: "Cloud Infrastructure - Multi-Cloud",
},
{
value: "CI_STORAGE",
label: "Cloud Infrastructure - Storage",
},
{
value: "CI_VMWARE",
label: "Cloud Infrastructure - VMware",
},
{
value: "DALH_DESIGN",
label: "Data Platform - Analytical Data Platform/Lakehouse",
},
{
value: "DATA_INTEGRATION",
label: "Data Platform - Data Integration",
},
{
value: "DM_UPGRADE",
label: "Data Platform - Modernise",
},
];
const ACTION_COMPLEXITY_OPTIONS = ["LOW", "MEDIUM", "HIGH", "VERY_HIGH"];
const appState = {
theme: getStoredTheme(),
sidebarCollapsed: getStoredSidebarCollapsed(),
calendarYear: new Date().getFullYear(),
calendarMonth: new Date().getMonth(),
calendarView: getStoredCalendarView(),
calendarAnchorDateKey: getLocalDateKey(new Date()),
status: "idle",
loadingMessage: "",
loadingProgress: 0,
errorMessage: "",
dataset: null,
sessionAuthHeaders: {},
detailModal: null,
actionFormModal: null,
rampComparisonModal: null,
forecastUpdateConfirmModal: null,
timeEntriesDrawer: null,
timeManagementModal: null,
taskTypeCache: {
promise: null,
},
timeManagementDatePicker: {
isOpen: false,
year: new Date().getFullYear(),
month: new Date().getMonth(),
},
pendingFocusSelector: "",
};
let syncFrame = 0;
let restoreFocusTarget = null;
let closeTimer = 0;
let requestCounter = 0;
let bridgeReadyPromise = null;
let cacheHydrationPromise = null;
let lastPersistedWorklistIframeUrl = "";
const pendingBridgeRequests = new Map();
function isTargetPage() {
const currentUrl = new URL(window.location.href);
return (
currentUrl.origin === TARGET_ORIGIN &&
currentUrl.pathname === TARGET_PATHNAME &&
currentUrl.searchParams.get(TARGET_PARAM) === TARGET_PARAM_VALUE
);
}
function isHcmWelcomePage() {
const currentUrl = new URL(window.location.href);
return (
currentUrl.origin === HCM_ORIGIN &&
currentUrl.pathname === HCM_WELCOME_PATHNAME
);
}
function scheduleSync() {
if (syncFrame) {
return;
}
syncFrame = window.requestAnimationFrame(() => {
syncFrame = 0;
syncButton();
});
}
function syncButton() {
const mount = document.getElementById(IDS.mount);
const hcmTile = document.getElementById(IDS.hcmTile);
if (isHcmWelcomePage()) {
mount?.remove();
ensureStyles();
ensureModal();
syncHcmTile();
ensureSpaFrame();
syncWorklistIframeToken();
return;
}
if (!isTargetPage()) {
mount?.remove();
hcmTile?.remove();
removeModal();
unlockScroll();
return;
}
hcmTile?.remove();
const header = document.querySelector(SELECTORS.header);
const functionsContainer = header?.querySelector(SELECTORS.functions);
const anchor = functionsContainer ? findUserAnchor(functionsContainer) : null;
if (!header || !functionsContainer) {
return;
}
ensureStyles();
const themedMount = mount ?? createMount();
const overlay = ensureModal();
applyTheme(header, functionsContainer, anchor, themedMount, overlay);
const targetSibling = resolveInsertionTarget(
functionsContainer,
anchor,
themedMount
);
if (
themedMount.parentElement !== functionsContainer ||
themedMount.nextElementSibling !== targetSibling
) {
const verifiedTarget =
targetSibling && targetSibling.parentElement === functionsContainer
? targetSibling
: null;
if (verifiedTarget) {
functionsContainer.insertBefore(themedMount, verifiedTarget);
} else {
functionsContainer.appendChild(themedMount);
}
}
}
function syncHcmTile() {
const appsGroup = document.getElementById(HCM_MY_INFORMATION_APPS_GROUP_ID);
const addTile = appsGroup?.querySelector(".flat-grid-cell.flat-grid-cell-addicon");
if (!addTile?.parentElement) {
document.getElementById(IDS.hcmTile)?.remove();
return;
}
const tile = document.getElementById(IDS.hcmTile) || createHcmTile();
if (tile.parentElement !== addTile.parentElement || tile.nextElementSibling !== addTile) {
addTile.parentElement.insertBefore(tile, addTile);
}
}
function syncWorklistIframeToken(options = {}) {
if (!isHcmWelcomePage()) {
return null;
}
for (const frame of document.querySelectorAll("iframe")) {
const tokenPayload = extractWorklistIframeToken(frame);
if (tokenPayload?.token) {
persistWorklistIframeToken(tokenPayload, options);
return tokenPayload;
}
}
return null;
}
function extractWorklistIframeToken(frame) {
const rawSrc = cleanString(frame?.getAttribute?.("src") || frame?.src);
if (!rawSrc) {
return null;
}
try {
const url = new URL(rawSrc, window.location.href);
const token = cleanString(url.searchParams.get("token"));
if (
url.origin !== WORKLIST_ORIGIN ||
url.pathname !== WORKLIST_SAASUI_PATHNAME ||
!token
) {
return null;
}
return {
token,
url: url.toString(),
};
} catch {
return null;
}
}
function persistWorklistIframeToken(tokenPayload, options = {}) {
if (!options.force && tokenPayload.url === lastPersistedWorklistIframeUrl) {
return;
}
lastPersistedWorklistIframeUrl = tokenPayload.url;
void writeCachedWorklistToken(tokenPayload);
}
function createHcmTile() {
const template = document.createElement("template");
template.innerHTML = `
<div class="flat-grid-cell" id="${IDS.hcmTile}">
<div id="c_75628d297d10409585cc1d40bb36f59d" class="app-nav-item" filmstrip="Arch Panel" page="undefined" index="0" type="subcluster" title="Arch Panel" group="groupNode_sales" destinationurl="#" targetframe="_self" isdesturlexist="true" role="presentation">
<svg viewBox="0 0 48 48" style="fill:currentColor" class="svg-nav suiicon svg-bkgd11" data-icon="navi_dashboard" role="presentation" focusable="false"><path class="svg-shortcut" d="M28 42.5l-3 2.7v-1.7c-.4 0-1.4 0-2.5.6-1.3 1-1.5 1.6-1.5 1.6s-.4-1.2.8-2.7c1.2-1.6 2.6-1.7 3.2-1.6v-1.6l3 2.7z"></path><path class="svg-cluster" d="M28.5 41.3c.6 0 1.2.5 1.2 1.2s-.6 1.2-1.2 1.2-1.2-.5-1.2-1.2.5-1.2 1.2-1.2zm-4 0c.6 0 1.2.5 1.2 1.2s-.6 1.2-1.2 1.2c-.7 0-1.2-.5-1.2-1.2s.5-1.2 1.2-1.2zm-4 0c.7 0 1.2.5 1.2 1.2s-.5 1.2-1.2 1.2-1.2-.5-1.2-1.2.5-1.2 1.2-1.2z"></path><path class="svg-icon18" d="M38 16H10v18.8c0 .7.5 1.2 1 1.2h26c.7 0 1.2-.5 1.2-1.2V16zm-9 3.2c0-.7.5-1.2 1-1.2h5c.5 0 1 .5 1 1.2v5.6c0 .7-.5 1.2-1 1.2h-5c-.5 0-1-.5-1-1.2v-5.6zM21 33c0 .6-.3 1-1 1h-7c-.7 0-1-.5-1-1.2V19.2c0-.7.5-1.2 1.2-1.2h6.6c.7 0 1.2.4 1.2 1v14zm1-13.8c0-.7.5-1.2 1-1.2h4c.5 0 1 .5 1 1.2v5.6c0 .7-.5 1.2-1 1.2h-4c-.5 0-1-.5-1-1.2v-5.6zM33 33H22v-2h11v2zm3-3H22v-2h14v2z"></path><path class="svg-icon14" d="M30 25v-5h1v4h4v1h-5zm4-5h1v3h-1v-3zm-2 1h1v2h-1v-2z"></path><path class="svg-icon03" d="M23 20h4v1h-4v-1zm3 4h-3v-1h3v1z"></path><path class="svg-icon04" d="M17 20c1 0 2 1 2 2h-2v-2zm-1 5c-1 0-2-1.2-2-2 0-1 1-2 2-2v2h2c0 1-1 2-2 2zm3 4h-5v-1h5v1zm0 3h-5v-1h5v1z"></path><path class="svg-icon05" d="M11 12h26c.7 0 1.2.6 1.2 1.2V15H10v-1.8c0-.6.5-1.2 1-1.2z"></path><path class="svg-outline" d="M38.1 35.76H9.92a1.16 1.16 0 0 1-1.15-1.17l.01-18.78h30.47V34.6a1.16 1.16 0 0 1-1.15 1.17zM23.9 19.31h2.53a1.15 1.15 0 0 1 1.13 1.17v3.45a1.15 1.15 0 0 1-1.13 1.17h-2.52a1.15 1.15 0 0 1-1.14-1.17v-3.45a1.15 1.15 0 0 1 1.14-1.17zm8.2.03h2.55a1.15 1.15 0 0 1 1.12 1.17v3.42a1.15 1.15 0 0 1-1.12 1.17H32.1a1.15 1.15 0 0 1-1.13-1.17v-3.42a1.15 1.15 0 0 1 1.13-1.17zm-18.6-.03h4.6a1.04 1.04 0 0 1 1.18 1v12.07a.9.9 0 0 1-1 1h-4.97a1.03 1.03 0 0 1-1-1.17V20.48a1.18 1.18 0 0 1 1.19-1.17z"></path><path class="svg-outline" d="M9.93 12.3H38.1a1.21 1.21 0 0 1 1.16 1.23l.02 2.28H8.78l-.01-2.28a1.21 1.21 0 0 1 1.16-1.23zM36 28.66H22m11 3.46H22"></path></svg>
<a id="c_75628d297d10409585cc1d40bb36f48d_0" class="app-nav-label flat-grid-nav-label" href="#">Arch Panel</a>
</div>
</div>
`;
const tile = template.content.firstElementChild;
tile.addEventListener("click", openHcmArchPanelTile);
tile.addEventListener("keydown", (event) => {
if (event.key === "Enter" || event.key === " ") {
openHcmArchPanelTile(event);
}
});
tile.tabIndex = 0;
tile.setAttribute("role", "button");
tile.setAttribute("aria-label", "Open Arch Panel");
return tile;
}
function openHcmArchPanelTile(event) {
event.preventDefault();
event.stopPropagation();
openModal();
}
function createMount() {
const mount = document.createElement("div");
mount.id = IDS.mount;
mount.className = "arch-panel-extension-entry";
const button = document.createElement("button");
button.id = IDS.button;
button.type = "button";
button.className = "arch-panel-extension-button";
button.textContent = "Arch panel";
button.setAttribute("aria-label", "Open Arch panel");
button.addEventListener("click", openModal);
mount.appendChild(button);
return mount;
}
function ensurePageBridge() {
if (bridgeReadyPromise) {
return bridgeReadyPromise;
}
bridgeReadyPromise = new Promise((resolve) => {
const existingScript = document.getElementById(BRIDGE.scriptId);
if (existingScript) {
resolve();
return;
}
const script = document.createElement("script");
script.id = BRIDGE.scriptId;
script.src = chrome.runtime.getURL("page-bridge.js");
script.async = false;
script.onload = () => {
script.remove();
resolve();
};
script.onerror = () => {
resolve();
};
(document.head || document.documentElement).appendChild(script);
});
return bridgeReadyPromise;
}
function ensureModal() {
const existingOverlay = document.getElementById(IDS.overlay);
if (existingOverlay) {
return existingOverlay;
}
const overlay = document.createElement("div");
overlay.id = IDS.overlay;
overlay.className = "arch-panel-extension-overlay";
overlay.hidden = true;
overlay.setAttribute("aria-hidden", "true");
overlay.addEventListener("click", handleOverlayClick);
overlay.addEventListener("change", handleOverlayChange);
overlay.addEventListener("input", handleOverlayInput);
overlay.addEventListener("keydown", handleOverlayKeydown);
document.body.appendChild(overlay);
renderModal();
return overlay;
}
function handleOverlayClick(event) {
const overlay = document.getElementById(IDS.overlay);
if (!overlay) {
return;
}
if (event.target === overlay) {
if (appState.actionFormModal) {
appState.actionFormModal = null;
renderModal();
} else if (appState.timeManagementModal) {
appState.timeManagementModal = null;
renderModal();
} else if (appState.forecastUpdateConfirmModal) {
appState.forecastUpdateConfirmModal = null;
renderModal();
} else if (appState.rampComparisonModal) {
appState.rampComparisonModal = null;
renderModal();
} else if (appState.detailModal) {
closeDetailModal();
renderModal();
} else {
closeModal();
}
return;
}
const actionElement = event.target.closest("[data-action]");
if (!actionElement) {
return;
}
const action = actionElement.getAttribute("data-action");
if (
action === "time-management-date-change" ||
action === "time-management-sr-change" ||
action === "time-management-activity-change" ||
action === "time-management-task-type-change"
) {
return;
}
event.preventDefault();
if (action === "close") {
closeModal();
return;
}
if (action === "toggle-theme") {
toggleTheme();
return;
}
if (action === "toggle-sidebar") {
toggleSidebar();
return;
}
if (action === "refresh") {
void refreshData();
return;
}
if (action === "calendar-prev") {
shiftCalendarPeriod(-1);
return;
}
if (action === "calendar-next") {
shiftCalendarPeriod(1);
return;
}
if (action === "calendar-today") {
goToCurrentCalendarPeriod();
return;
}
if (action === "calendar-view") {
const calendarView = actionElement.getAttribute("data-calendar-view");
if (calendarView) {
setCalendarView(calendarView);
}
return;
}
if (action === "download-json") {
if (appState.dataset) {
downloadFile(
"arch-panel-data.json",
JSON.stringify(appState.dataset.exportPayload, null, 2),
"application/json"
);
}
return;
}
if (action === "download-html") {
if (appState.dataset) {
downloadFile(
"arch-panel-dashboard.html",
createExportHtml(appState.dataset),
"text/html"
);
}
return;
}
if (action === "open-detail") {
const detailScope = actionElement.getAttribute("data-detail-scope");
const detailValue = actionElement.getAttribute("data-detail-value");
openDetailModal(detailScope, detailValue);
return;
}
if (action === "open-action-form") {
const workloadId = actionElement.getAttribute("data-workload-id");
if (workloadId) {
openActionFormModal(workloadId);
}
return;
}
if (action === "open-ramp-comparison") {
const workloadId = actionElement.getAttribute("data-workload-id");
if (workloadId) {
openRampComparisonModal(workloadId);
}
return;
}
if (action === "close-ramp-comparison") {
appState.forecastUpdateConfirmModal = null;
appState.rampComparisonModal = null;
renderModal();
return;
}
if (action === "open-forecast-update-confirm") {
openForecastUpdateConfirmModal();
return;
}
if (action === "close-forecast-update-confirm") {
appState.forecastUpdateConfirmModal = null;
renderModal();
return;
}
if (action === "submit-forecast-update") {
void submitForecastUpdate();
return;
}
if (action === "close-action-form") {
appState.actionFormModal = null;
renderModal();
return;
}
if (action === "open-time-entries") {
const srNumber = actionElement.getAttribute("data-sr-number");
if (srNumber) {
void openTimeEntriesDrawer(srNumber);
}
return;
}
if (action === "close-time-entries") {
appState.timeEntriesDrawer = null;
renderModal();
return;
}
if (action === "retry-time-entries") {
const srNumber = appState.timeEntriesDrawer?.srNumber;
if (srNumber) {
void openTimeEntriesDrawer(srNumber);
}
return;
}
if (action === "open-time-management") {
void openTimeManagementModal();
return;
}
if (action === "close-time-management") {
appState.timeManagementModal = null;
renderModal();
return;
}
if (action === "time-management-add-sr") {
addTimeManagementEntryRow("sr");
return;
}
if (action === "time-management-add-non-service") {
addTimeManagementEntryRow("non-service");
return;
}
if (action === "time-management-remove-row") {
removeTimeManagementEntryRow(actionElement.getAttribute("data-row-id"));
return;
}
if (action === "open-time-management-date-picker") {
if (!actionElement.matches("button.arch-panel-extension-date-picker-button")) {
return;
}
toggleTimeManagementDatePicker();
return;
}
if (action === "time-management-date-week-prev") {
void shiftTimeManagementDateByWeeks(-1);
return;
}
if (action === "time-management-date-week-next") {
void shiftTimeManagementDateByWeeks(1);
return;
}
if (action === "time-management-date-picker-prev") {
shiftTimeManagementDatePickerMonth(-1);
return;
}
if (action === "time-management-date-picker-next") {
shiftTimeManagementDatePickerMonth(1);
return;
}
if (action === "time-management-date-picker-select") {
const selectedDate = actionElement.getAttribute("data-date-value");
if (selectedDate) {
void selectTimeManagementDate(selectedDate);
}
return;
}
if (action === "time-management-combobox-option") {
selectTimeManagementComboboxOption(
actionElement.getAttribute("data-field"),
actionElement.getAttribute("data-value"),
actionElement.getAttribute("data-row-id")
);
return;
}
if (action === "submit-action-form") {
const form = actionElement.closest("form");
if (form instanceof HTMLFormElement) {
void submitActionForm(form);
}
return;
}
if (action === "close-detail") {
closeDetailModal();
renderModal();
return;
}
if (action === "sort-detail") {
const sortKey = actionElement.getAttribute("data-sort-key");
if (sortKey) {
toggleDetailSort(sortKey);
}
}
}
function handleOverlayChange(event) {
const target = event.target;
if (!(target instanceof HTMLInputElement || target instanceof HTMLSelectElement)) {
return;
}
const action = target.getAttribute("data-action");
if (action === "time-management-date-change") {
void loadTimeManagementData(target.value);
return;
}
if (action === "time-management-sr-change") {
updateTimeManagementSelectedSr(target.value, target.getAttribute("data-row-id"));
return;
}
if (action === "time-management-activity-change") {
updateTimeManagementField(
"selectedActivityValue",
target.value,
target.getAttribute("data-row-id")
);
return;
}
if (action === "time-management-task-type-change") {
updateTimeManagementField(
"selectedTaskTypeValue",
target.value,
target.getAttribute("data-row-id")
);
return;
}
if (action === "time-management-hours-change") {
updateTimeManagementDayHours(
target.getAttribute("data-row-id"),
target.getAttribute("data-date-key"),
target.value
);
}
}
function handleOverlayInput(event) {
const target = event.target;
if (!(target instanceof HTMLInputElement)) {
return;
}
const field = target.getAttribute("data-combobox-field");
if (!field) {
return;
}
filterTimeManagementCombobox(target);
}
function handleOverlayKeydown(event) {
const target = event.target;
if (!(target instanceof HTMLInputElement)) {
return;
}
if (target.getAttribute("data-action") === "time-management-hours-change") {
handleTimeManagementHourKeydown(event, target);
return;
}
const field = target.getAttribute("data-combobox-field");
if (!field) {
return;
}
if (![
"ArrowDown",
"ArrowUp",
"Home",
"End",
"Enter",
"Escape",
].includes(event.key)) {
return;
}
handleTimeManagementComboboxKeydown(event, target);
}
function handleTimeManagementHourKeydown(event, input) {
if (
![
"ArrowLeft",
"ArrowRight",
"ArrowUp",
"ArrowDown",
"Home",
"End",
"Enter",
].includes(event.key)
) {
return;
}
const nextInput = getNextTimeManagementHourInput(input, event);
if (!nextInput) {
return;
}
event.preventDefault();
nextInput.focus();
nextInput.select();
}
function getNextTimeManagementHourInput(currentInput, event) {
const rows = Array.from(
document.querySelectorAll(
".arch-panel-extension-time-management-entry-row[data-row-id]"
)
);
const currentRow = currentInput.closest(
".arch-panel-extension-time-management-entry-row"
);
const rowIndex = rows.indexOf(currentRow);
if (rowIndex < 0) {
return null;
}
const currentInputs = getTimeManagementHourInputs(rows[rowIndex]);
const columnIndex = currentInputs.indexOf(currentInput);
if (columnIndex < 0) {
return null;
}
let nextRowIndex = rowIndex;
let nextColumnIndex = columnIndex;
if (event.key === "ArrowLeft") {
nextColumnIndex -= 1;
} else if (event.key === "ArrowRight") {
nextColumnIndex += 1;
} else if (event.key === "ArrowUp") {
nextRowIndex -= 1;
} else if (event.key === "ArrowDown" || event.key === "Enter") {
nextRowIndex += event.shiftKey ? -1 : 1;
} else if (event.key === "Home") {
nextColumnIndex = 0;
} else if (event.key === "End") {
nextColumnIndex = currentInputs.length - 1;
}
const nextRow = rows[nextRowIndex];
if (!nextRow) {
return null;
}
const nextInputs = getTimeManagementHourInputs(nextRow);
return nextInputs[nextColumnIndex] || null;
}
function getTimeManagementHourInputs(row) {
return Array.from(
row.querySelectorAll('input[data-action="time-management-hours-change"]')
);
}
function toggleTimeManagementDatePicker() {
const selectedDate = getTimeManagementSelectedDate();
const pickerDate = parseDatePreservingDateOnly(selectedDate);
const fallback = Number.isNaN(pickerDate.getTime()) ? new Date() : pickerDate;
appState.timeManagementDatePicker = {
isOpen: !appState.timeManagementDatePicker.isOpen,
year: fallback.getFullYear(),
month: fallback.getMonth(),
};
renderModal();
}
function shiftTimeManagementDatePickerMonth(delta) {
const picker = appState.timeManagementDatePicker;
const nextDate = new Date(picker.year, picker.month + delta, 1);
appState.timeManagementDatePicker = {
isOpen: true,
year: nextDate.getFullYear(),
month: nextDate.getMonth(),
};
renderModal();
}
async function selectTimeManagementDate(selectedDate) {
appState.timeManagementDatePicker = {
...appState.timeManagementDatePicker,
isOpen: false,
};
await loadTimeManagementData(selectedDate);
}
async function shiftTimeManagementDateByWeeks(delta) {
const selectedDate = getTimeManagementSelectedDate();
const parsedDate = parseDatePreservingDateOnly(selectedDate);
const baseDate = Number.isNaN(parsedDate.getTime()) ? new Date() : parsedDate;
baseDate.setDate(baseDate.getDate() + (Number(delta) || 0) * 7);
appState.timeManagementDatePicker = {
...appState.timeManagementDatePicker,
isOpen: false,
};
await loadTimeManagementData(getDateInputValue(baseDate));
}
function getTimeManagementSelectedDate() {
return appState.timeManagementModal?.selectedDate || getDateInputValue(new Date());
}
function renderModal() {
const overlay = document.getElementById(IDS.overlay);
if (!overlay) {
return;
}
const previousScrollState = captureModalScrollState(overlay);
const activeFocusSelector = getRestorableFocusSelector(document.activeElement);
const requestedFocusSelector = appState.pendingFocusSelector || activeFocusSelector;
overlay.innerHTML = createModalMarkup();
applyDialogTheme(overlay);
restoreModalScrollState(overlay, previousScrollState);
window.requestAnimationFrame(() => {
if (
!appState.detailModal &&
!appState.actionFormModal &&
!appState.rampComparisonModal &&
!appState.forecastUpdateConfirmModal &&
!appState.timeManagementModal &&
requestedFocusSelector
) {
const focusTarget = overlay.querySelector(requestedFocusSelector);
if (focusTarget instanceof HTMLElement) {
focusTarget.focus({ preventScroll: true });
}
}
appState.pendingFocusSelector = "";
});
}
function captureModalScrollState(overlay) {
const mainShell = overlay.querySelector(".arch-panel-extension-main-shell");
const shellBody = overlay.querySelector(".arch-panel-extension-shell-body");
const dialog = overlay.querySelector(".arch-panel-extension-dialog");
const detailTable = overlay.querySelector(
".arch-panel-extension-table-wrap.is-detail"
);
return {
windowX: window.scrollX,
windowY: window.scrollY,
documentTop: document.documentElement.scrollTop,
bodyTop: document.body.scrollTop,
mainShellTop: mainShell ? mainShell.scrollTop : 0,
shellBodyTop: shellBody ? shellBody.scrollTop : 0,
dialogTop: dialog ? dialog.scrollTop : 0,
detailTableTop: detailTable ? detailTable.scrollTop : 0,
detailTableLeft: detailTable ? detailTable.scrollLeft : 0,
};
}
function restoreModalScrollState(overlay, scrollState) {
const restore = () => {
const mainShell = overlay.querySelector(".arch-panel-extension-main-shell");
const shellBody = overlay.querySelector(".arch-panel-extension-shell-body");
const dialog = overlay.querySelector(".arch-panel-extension-dialog");
const detailTable = overlay.querySelector(
".arch-panel-extension-table-wrap.is-detail"
);
if (mainShell) {
mainShell.scrollTop = scrollState.mainShellTop;
}
if (shellBody) {
shellBody.scrollTop = scrollState.shellBodyTop;
}
if (dialog) {
dialog.scrollTop = scrollState.dialogTop;
}
if (detailTable) {
detailTable.scrollTop = scrollState.detailTableTop;
detailTable.scrollLeft = scrollState.detailTableLeft;
}
document.documentElement.scrollTop = scrollState.documentTop;
document.body.scrollTop = scrollState.bodyTop;
window.scrollTo(scrollState.windowX, scrollState.windowY);
};
window.requestAnimationFrame(() => {
restore();
window.requestAnimationFrame(restore);
});
}
function getRestorableFocusSelector(element) {
if (!(element instanceof HTMLElement)) {
return "";
}
const action = element.getAttribute("data-action");
if (
action === "calendar-prev" ||
action === "calendar-next" ||
action === "calendar-today"
) {
return `[data-action="${action}"]`;
}
if (action === "calendar-view") {
const calendarView = element.getAttribute("data-calendar-view");
return calendarView
? `[data-action="calendar-view"][data-calendar-view="${calendarView}"]`
: `[data-action="${action}"]`;
}
const detailScope = element.getAttribute("data-detail-scope");
const detailValue = element.getAttribute("data-detail-value");
if (detailScope === "calendar-day" && detailValue) {
return getCalendarDayFocusSelector(detailValue);
}
return "";
}
function getCalendarDayFocusSelector(dateKey) {
return `[data-action="open-detail"][data-detail-scope="calendar-day"][data-detail-value="${dateKey}"]`;
}
function createModalMarkup() {
return `
<div
id="${IDS.dialog}"
class="arch-panel-extension-dialog"
role="dialog"
aria-modal="true"
aria-labelledby="arch-panel-extension-title"
tabindex="-1"
>
<header class="arch-panel-extension-shell-header">
<div class="arch-panel-extension-brand">
<span class="arch-panel-extension-brand-mark">Arch</span>
<div>
<p class="arch-panel-extension-brand-label">Enterprise workload intelligence</p>
<h1 id="arch-panel-extension-title" class="arch-panel-extension-title">
Arch panel
</h1>
</div>
</div>
<div class="arch-panel-extension-shell-actions">
<span class="arch-panel-extension-shell-note">
${getHeaderSummary()}
</span>
<button
type="button"
class="arch-panel-extension-theme-toggle"
data-action="toggle-sidebar"
aria-label="Toggle sidebar"
>
${appState.sidebarCollapsed ? "Show sidebar" : "Hide sidebar"}
</button>
<button
type="button"
class="arch-panel-extension-theme-toggle"
data-action="toggle-theme"
aria-label="Toggle dashboard theme"
>
${appState.theme === "dark" ? "Light mode" : "Dark mode"}
</button>
<button
type="button"
class="arch-panel-extension-close"
data-action="close"
aria-label="Close Arch panel"
>
Close
</button>
</div>
</header>
<div class="arch-panel-extension-shell-body${appState.sidebarCollapsed ? " is-sidebar-collapsed" : ""}">
${renderMainContent()}
${renderDetailModal()}
${renderActionFormModal()}
${renderRampComparisonModal()}
${renderForecastUpdateConfirmModal()}
${renderTimeEntriesDrawer()}
${renderTimeManagementModal()}
</div>
</div>
`;
}
function renderMainContent() {
if (appState.status === "loading") {
return `
<main class="arch-panel-extension-main-shell">
<section class="arch-panel-extension-state-card">
<p class="arch-panel-extension-state-eyebrow">Loading data</p>
<h2 class="arch-panel-extension-state-title">Building the workload dataset</h2>
<p class="arch-panel-extension-state-copy">${escapeHtml(appState.loadingMessage)}</p>
<div
class="arch-panel-extension-loading-progress"
role="progressbar"
aria-valuemin="0"
aria-valuemax="100"
aria-valuenow="${Math.round(appState.loadingProgress || 0)}"
>
<div class="arch-panel-extension-loading-progress-meta">
<span>Overall progress</span>
</div>
<div class="arch-panel-extension-loading-progress-track">
<div
class="arch-panel-extension-loading-progress-fill"
style="width: 0%;"
></div>
<span class="arch-panel-extension-loading-progress-value">
${formatProgressPercent(appState.loadingProgress || 0)}
</span>
</div>
</div>
</section>
</main>
`;
}
if (appState.status === "error") {
return `
<main class="arch-panel-extension-main-shell">
<section class="arch-panel-extension-state-card is-error">
<p class="arch-panel-extension-state-eyebrow">Request failed</p>
<h2 class="arch-panel-extension-state-title">Unable to assemble the dashboard</h2>
<p class="arch-panel-extension-state-copy">${escapeHtml(appState.errorMessage)}</p>
<div class="arch-panel-extension-state-actions">
<button type="button" class="arch-panel-extension-utility-button" data-action="refresh">
Retry
</button>
</div>
</section>
</main>
`;
}
if (!appState.dataset) {
return `
<main class="arch-panel-extension-main-shell">
<section class="arch-panel-extension-state-card">
<p class="arch-panel-extension-state-eyebrow">Ready</p>
<h2 class="arch-panel-extension-state-title">Start the live data extraction</h2>
<p class="arch-panel-extension-state-copy">
No cached snapshot is available yet. Use Refresh data to fetch the current user, customers, workloads, actions and service requests.
</p>
<div class="arch-panel-extension-state-actions">
<button type="button" class="arch-panel-extension-utility-button" data-action="refresh">
Refresh data
</button>
</div>
</section>
</main>
`;
}
const { user, summary } = appState.dataset;
return `
<aside class="arch-panel-extension-sidebar${appState.sidebarCollapsed ? " is-collapsed" : ""}" aria-label="Dataset summary">
<div class="arch-panel-extension-sidebar-group">
<p class="arch-panel-extension-sidebar-label">Current user</p>
${renderCurrentUserSidebar(user)}
</div>
<div class="arch-panel-extension-sidebar-group">
<p class="arch-panel-extension-sidebar-label">Exports</p>
<button type="button" class="arch-panel-extension-sidebar-button" data-action="download-json">
Download JSON
</button>
<button type="button" class="arch-panel-extension-sidebar-button" data-action="download-html">
Download HTML
</button>
<button type="button" class="arch-panel-extension-sidebar-button" data-action="refresh">
Refresh data
</button>
</div>
<div class="arch-panel-extension-sidebar-group">
<p class="arch-panel-extension-sidebar-label">Coverage</p>
<div class="arch-panel-extension-sidebar-data">
<span>Customers: ${formatWholeNumber(summary.totalCustomers)}</span>
<span>Workloads: ${formatWholeNumber(summary.totalWorkloads)}</span>
<span>Pending SRs: ${formatWholeNumber(summary.pendingServiceRequests.length)}</span>
</div>
</div>
<div class="arch-panel-extension-sidebar-group">
<p class="arch-panel-extension-sidebar-label">Last refresh</p>
<div class="arch-panel-extension-sidebar-data">
<strong>${escapeHtml(summary.generatedAtLabel || "-")}</strong>
<span>Updated by Refresh data</span>
</div>
</div>
<div class="arch-panel-extension-sidebar-group">
<p class="arch-panel-extension-sidebar-label">Extension version</p>
<div class="arch-panel-extension-sidebar-data">
<span>v${escapeHtml(getExtensionVersion())}</span>
</div>
</div>
</aside>
<main class="arch-panel-extension-main-shell">
<section class="arch-panel-extension-summary-row" aria-label="Overview metrics">
${renderMetricCard("Total clients", formatWholeNumber(summary.totalCustomers), "Loaded from listSummary")}
${renderMetricCard("Total workloads", formatWholeNumber(summary.totalWorkloads), "Excluding Won workloads")}
${renderMetricCard("Total (ACR)", formatCurrency(summary.totalEligibleAcr), "Across eligible workloads")}
${renderMetricCard("Pending SRs", formatWholeNumber(summary.pendingServiceRequests.length), "SVC_PENDING_DELIVERY")}
</section>
<section class="arch-panel-extension-indicator-grid" aria-label="Opportunity indicators">
${summary.stageIndicators.map((indicator) => renderStageIndicator(indicator)).join("")}
</section>
<section class="arch-panel-extension-status-grid" aria-label="SR and action status indicators">
${renderBooleanIndicator(
"Status de SRs",
"Workloads com SR",
summary.srStatus.trueCount,
"Workloads sem SR",
summary.srStatus.falseCount,
"sr"
)}
${renderBooleanIndicator(
"Status de Consumption Plan/action",
"Workloads com action",
summary.actionStatus.trueCount,
"Workloads sem action",
summary.actionStatus.falseCount,
"action"
)}
</section>
${renderSrHoursAndCalendarSection(summary)}
</main>
`;
}
function renderCurrentUserSidebar(user) {
const displayName = cleanString(user?.name || user?.userEmail);
const userEmail = cleanString(user?.userEmail);
const avatar = cleanString(user?.avatar);
const initials = getUserInitials(displayName || userEmail);
return `
<div class="arch-panel-extension-current-user-card">
${
avatar
? `<img
class="arch-panel-extension-current-user-avatar"
src="${escapeHtml(avatar)}"
alt="${escapeHtml(displayName ? `${displayName} avatar` : "User avatar")}"
loading="lazy"
referrerpolicy="no-referrer"
>`
: `<span class="arch-panel-extension-current-user-avatar is-fallback" aria-hidden="true">${escapeHtml(initials)}</span>`
}
<div class="arch-panel-extension-sidebar-data">
<strong>${escapeHtml(displayName || userEmail)}</strong>
<span>${escapeHtml(userEmail)}</span>
</div>
</div>
`;
}
function getUserInitials(value) {
const parts = cleanString(value)
.split(/\s+/)
.filter(Boolean)
.slice(0, 2);
if (!parts.length) {
return "U";
}
return parts.map((part) => part.charAt(0).toUpperCase()).join("");
}
function getExtensionVersion() {
try {
return chrome.runtime.getManifest()?.version || "0.0.0";
} catch {
return "0.0.0";
}
}
function renderMetricCard(label, value, detail) {
return `
<article class="arch-panel-extension-summary-card">
<p class="arch-panel-extension-summary-label">${escapeHtml(label)}</p>
<p class="arch-panel-extension-summary-value">${escapeHtml(value)}</p>
<p class="arch-panel-extension-summary-detail">${escapeHtml(detail)}</p>
</article>
`;
}
function renderStageIndicator(indicator) {
return `
<button
type="button"
class="arch-panel-extension-summary-card arch-panel-extension-summary-card-button"
data-action="open-detail"
data-detail-scope="stage"
data-detail-value="${escapeHtml(indicator.label)}"
>
<p class="arch-panel-extension-summary-label">${escapeHtml(indicator.label)}</p>
<p class="arch-panel-extension-summary-value">${formatWholeNumber(indicator.count)}</p>
<p class="arch-panel-extension-summary-detail">Total (ACR): ${formatCurrency(indicator.totalAcr)}</p>
</button>
`;
}
function renderBooleanIndicator(
title,
trueLabel,
trueCount,
falseLabel,
falseCount,
scope
) {
const total = trueCount + falseCount;
const truePercent = total > 0 ? (trueCount / total) * 100 : 0;
const falsePercent = total > 0 ? (falseCount / total) * 100 : 0;
return `
<article class="arch-panel-extension-panel">
<header class="arch-panel-extension-panel-head">
<div>
<p class="arch-panel-extension-panel-label">${escapeHtml(title)}</p>
</div>
</header>
<div class="arch-panel-extension-progress">
<div class="arch-panel-extension-progress-track">
<span
class="arch-panel-extension-progress-segment is-success"
style="width: ${truePercent}%;"
></span>
<span
class="arch-panel-extension-progress-segment is-danger"
style="width: ${falsePercent}%;"
></span>
</div>
<div class="arch-panel-extension-progress-labels">
<span>${escapeHtml(trueLabel)}: ${formatPercent(truePercent)}</span>
<span>${escapeHtml(falseLabel)}: ${formatPercent(falsePercent)}</span>
</div>
</div>
<div class="arch-panel-extension-boolean-grid">
<button
type="button"
class="arch-panel-extension-summary-card arch-panel-extension-summary-card-button"
data-action="open-detail"
data-detail-scope="${escapeHtml(scope)}"
data-detail-value="true"
>
<p class="arch-panel-extension-summary-label">${escapeHtml(trueLabel)}</p>
<p class="arch-panel-extension-summary-value">${formatWholeNumber(trueCount)}</p>
<p class="arch-panel-extension-summary-detail">${formatPercent(truePercent)}</p>
</button>
<button
type="button"
class="arch-panel-extension-summary-card arch-panel-extension-summary-card-button"
data-action="open-detail"
data-detail-scope="${escapeHtml(scope)}"
data-detail-value="false"
>
<p class="arch-panel-extension-summary-label">${escapeHtml(falseLabel)}</p>
<p class="arch-panel-extension-summary-value">${formatWholeNumber(falseCount)}</p>
<p class="arch-panel-extension-summary-detail">${formatPercent(falsePercent)}</p>
</button>
</div>
</article>
`;
}
function renderSrHoursAndCalendarSection(summary) {
const calendar = buildWorkloadStartCalendar(
summary.allWorkloads,
appState.calendarYear,
appState.calendarMonth,
appState.calendarView,
appState.calendarAnchorDateKey
);
return `
<section class="arch-panel-extension-operations-grid" aria-label="Pending SRs and workload calendar">
${renderSrHoursIndicatorCard(summary)}
${renderWorkloadStartCalendar(calendar)}
</section>
`;
}
function renderSrHoursIndicatorCard(summary) {
const zeroToFourCount = summary.srHours.betweenZeroAndFour.length;
const aboveFourCount = summary.srHours.aboveFour.length;
const total = zeroToFourCount + aboveFourCount;
const zeroToFourPercent = total > 0 ? (zeroToFourCount / total) * 100 : 0;
const aboveFourPercent = total > 0 ? (aboveFourCount / total) * 100 : 0;
return `
<article class="arch-panel-extension-panel">
<header class="arch-panel-extension-panel-head">
<div>
<p class="arch-panel-extension-panel-label">Horas apontadas</p>
</div>
</header>
<div class="arch-panel-extension-progress">
<div class="arch-panel-extension-progress-track">
<span
class="arch-panel-extension-progress-segment is-success"
style="width: ${zeroToFourPercent}%;"
></span>
<span
class="arch-panel-extension-progress-segment is-danger"
style="width: ${aboveFourPercent}%;"
></span>
</div>
<div class="arch-panel-extension-progress-labels">
<span>SRs 0-4h: ${formatPercent(zeroToFourPercent)}</span>
<span>SRs &gt;4h: ${formatPercent(aboveFourPercent)}</span>
</div>
</div>
<div class="arch-panel-extension-boolean-grid">
${renderSrHoursIndicator(
"SRs 0-4h",
formatPercent(zeroToFourPercent),
zeroToFourCount,
"0-4"
)}
${renderSrHoursIndicator(
"SRs >4h",
formatPercent(aboveFourPercent),
aboveFourCount,
"above-4"
)}
</div>
</article>
`;
}
function renderSrHoursIndicator(label, detail, count, value) {
return `
<button
type="button"
class="arch-panel-extension-summary-card arch-panel-extension-summary-card-button"
data-action="open-detail"
data-detail-scope="sr-hours"
data-detail-value="${escapeHtml(value)}"
aria-label="Open ${escapeHtml(label)} service requests"
>
<p class="arch-panel-extension-summary-label">${escapeHtml(label)}</p>
<p class="arch-panel-extension-summary-value">${formatWholeNumber(count)}</p>
<p class="arch-panel-extension-summary-detail">${escapeHtml(detail)}</p>
</button>
`;
}
function renderWorkloadStartCalendar(calendar) {
return `
<article class="arch-panel-extension-panel arch-panel-extension-calendar-card">
<header class="arch-panel-extension-panel-head">
<div>
<p class="arch-panel-extension-panel-label">Workload start calendar</p>
<h3 class="arch-panel-extension-panel-title">${escapeHtml(calendar.periodLabel)}</h3>
</div>
<div class="arch-panel-extension-calendar-actions">
<div class="arch-panel-extension-calendar-view-toggle" aria-label="Calendar view">
${["month", "week"]
.map(
(view) => `
<button
type="button"
class="arch-panel-extension-calendar-view-button${
calendar.view === view ? " is-active" : ""
}"
data-action="calendar-view"
data-calendar-view="${view}"
aria-pressed="${calendar.view === view ? "true" : "false"}"
>
${view === "month" ? "Month" : "Week"}
</button>
`
)
.join("")}
</div>
<button
type="button"
class="arch-panel-extension-calendar-nav"
data-action="calendar-prev"
aria-label="${calendar.view === "week" ? "Previous week" : "Previous month"}"
>
</button>
<span>${formatWholeNumber(calendar.totalVisibleWorkloads)} starts</span>
<button
type="button"
class="arch-panel-extension-calendar-today"
data-action="calendar-today"
aria-label="${calendar.view === "week" ? "Go to current week" : "Go to current month"}"
>
Today
</button>
<button
type="button"
class="arch-panel-extension-calendar-nav"
data-action="calendar-next"
aria-label="${calendar.view === "week" ? "Next week" : "Next month"}"
>
</button>
</div>
</header>
<div class="arch-panel-extension-calendar-weekdays${
calendar.view === "week" ? " is-week" : ""
}">
${["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]
.map((day) => `<span>${day}</span>`)
.join("")}
</div>
<div class="arch-panel-extension-calendar-grid${
calendar.view === "week" ? " is-week" : ""
}">
${calendar.days
.map((day) => renderCalendarDay(day, calendar.view))
.join("")}
</div>
</article>
`;
}
function renderCalendarDay(day, view = "month") {
if (view === "week") {
return renderCalendarWeekDay(day);
}
const firstWorkload = day.workloads[0];
const calendarWorkloadLabel =
firstWorkload?.description || firstWorkload?.name || "Workload";
const className = [
"arch-panel-extension-calendar-day",
day.isCurrentMonth ? "" : "is-muted",
day.isToday ? "is-today" : "",
day.workloads.length ? "has-workload" : "",
]
.filter(Boolean)
.join(" ");
return `
<button
type="button"
class="${className}"
title="${escapeHtml(
day.workloads
.map((workload) => workload.description || workload.name)
.filter(Boolean)
.join(" | ")
)}"
data-action="open-detail"
data-detail-scope="calendar-day"
data-detail-value="${escapeHtml(day.dateKey)}"
${day.workloads.length ? "" : "disabled"}
>
<span class="arch-panel-extension-calendar-date">${day.dayNumber}</span>
${
day.isWeekStart
? `<span class="arch-panel-extension-calendar-week-number">${escapeHtml(
day.operationalWeekLabel
)}</span>`
: ""
}
${
day.workloads.length
? `<span class="arch-panel-extension-calendar-workload">${escapeHtml(
calendarWorkloadLabel
)}</span><span class="arch-panel-extension-calendar-count">${formatWholeNumber(
day.workloads.length
)}</span>`
: ""
}
</button>
`;
}
function renderCalendarWeekDay(day) {
const className = [
"arch-panel-extension-calendar-day",
"is-week-day",
day.isToday ? "is-today" : "",
day.workloads.length ? "has-workload" : "",
]
.filter(Boolean)
.join(" ");
const title = day.workloads
.map((workload) => workload.description || workload.name)
.filter(Boolean)
.join(" | ");
return `
<button
type="button"
class="${className}"
title="${escapeHtml(title)}"
data-action="open-detail"
data-detail-scope="calendar-day"
data-detail-value="${escapeHtml(day.dateKey)}"
${day.workloads.length ? "" : "disabled"}
>
<span class="arch-panel-extension-calendar-week-day-head">
<span class="arch-panel-extension-calendar-date">${day.dayNumber}</span>
${
day.isWeekStart
? `<span class="arch-panel-extension-calendar-week-number">${escapeHtml(
day.operationalWeekLabel
)}</span>`
: ""
}
</span>
<span class="arch-panel-extension-calendar-week-workloads">
${
day.workloads.length
? day.workloads.map((workload) => renderCalendarWeekWorkload(workload)).join("")
: `<span class="arch-panel-extension-calendar-week-empty">No starts</span>`
}
</span>
</button>
`;
}
function renderCalendarWeekWorkload(workload) {
const label = workload.description || workload.name || "Workload";
const customer = workload.customerName || "";
return `
<span class="arch-panel-extension-calendar-week-workload">
<span class="arch-panel-extension-calendar-week-workload-title">${escapeHtml(label)}</span>
${
customer
? `<span class="arch-panel-extension-calendar-week-workload-meta">${escapeHtml(
customer
)}</span>`
: ""
}
</span>
`;
}
function renderWorkloadTable(workloads) {
return `
<div class="arch-panel-extension-table-wrap is-main">
<table class="arch-panel-extension-table">
<colgroup>
<col class="arch-panel-extension-col-opportunity">
<col class="arch-panel-extension-col-customer">
<col class="arch-panel-extension-col-workload">
<col class="arch-panel-extension-col-acr">
<col class="arch-panel-extension-col-type">
<col class="arch-panel-extension-col-flag">
<col class="arch-panel-extension-col-flag">
</colgroup>
<thead>
<tr>
<th>Opportunity</th>
<th>Customer</th>
<th>Workload</th>
<th class="is-numeric">ACR</th>
<th>Type</th>
<th>Tem SR?</th>
<th>Tem Action?</th>
</tr>
</thead>
<tbody>
${workloads.map((workload) => renderWorkloadTableRow(workload)).join("")}
</tbody>
</table>
</div>
`;
}
function renderWorkloadTableRow(
workload,
includeConsumptionStartDate = false,
includeRampMonths = false
) {
const hasRampMismatch =
includeRampMonths && hasRampForecastMismatch(workload);
return `
<tr>
<td>${renderOpportunityLink(workload)}</td>
<td>${escapeHtml(workload.customerName || "")}</td>
<td>${renderWorkloadLink(workload, hasRampMismatch)}</td>
${
includeConsumptionStartDate
? `<td>${escapeHtml(formatDate(workload.consumptionStartDate) || "-")}</td>`
: ""
}
${
includeRampMonths
? `<td class="is-numeric">${renderRampMonthsButton(workload)}</td>`
: ""
}
<td class="is-numeric">${formatCurrency(workload.adjustedACR)}</td>
<td>${escapeHtml(workload.opportunityForecastTypeGroup)}</td>
<td>${renderBooleanBadge(workload.hasSR)}</td>
<td>${renderHasActionCell(workload)}</td>
</tr>
`;
}
function renderRampMonthsButton(workload) {
return `
<button
type="button"
class="arch-panel-extension-row-link arch-panel-extension-inline-link-button"
data-action="open-ramp-comparison"
data-workload-id="${escapeHtml(workload.workloadId || "")}"
aria-label="Abrir comparação de rampa para ${escapeHtml(
workload.name || "workload"
)}"
>
${formatWholeNumber(workload.rampMonths || 0)}
</button>
`;
}
function renderBooleanBadge(value) {
const isTrue = Boolean(value);
const variantClass = isTrue ? "is-success" : "is-danger";
const label = isTrue ? "Sim" : "Não";
return `
<span class="arch-panel-extension-badge ${variantClass}">
${label}
</span>
`;
}
function renderHasActionCell(workload) {
if (workload.hasAction) {
return renderBooleanBadge(true);
}
return `
<div class="arch-panel-extension-action-cell">
${renderBooleanBadge(false)}
<button
type="button"
class="arch-panel-extension-add-action-button"
data-action="open-action-form"
data-workload-id="${escapeHtml(workload.workloadId || "")}"
aria-label="Criar action para ${escapeHtml(workload.name || "workload")}"
title="Criar action"
>
</button>
</div>
`;
}
function renderInsightRow(label, value, detail) {
return `
<div class="arch-panel-extension-insight">
<div>
<p class="arch-panel-extension-insight-label">${escapeHtml(label)}</p>
<p class="arch-panel-extension-insight-detail">${escapeHtml(detail)}</p>
</div>
<p class="arch-panel-extension-insight-value">${escapeHtml(value)}</p>
</div>
`;
}
function renderDetailModal() {
if (!appState.detailModal) {
return "";
}
const isServiceRequestDetail = appState.detailModal.type === "serviceRequests";
const detailItems = isServiceRequestDetail
? appState.detailModal.items || []
: getSortedDetailItems();
return `
<div class="arch-panel-extension-detail-overlay">
<div class="arch-panel-extension-detail-dialog" role="document">
<header class="arch-panel-extension-detail-head">
<div>
<p class="arch-panel-extension-panel-label">${escapeHtml(appState.detailModal.label)}</p>
<h3 class="arch-panel-extension-panel-title">${escapeHtml(appState.detailModal.title)}</h3>
</div>
</header>
<div class="arch-panel-extension-detail-meta">
${
isServiceRequestDetail
? `<span>${formatWholeNumber(detailItems.length)} SRs</span>`
: `<span>${formatWholeNumber(detailItems.length)} workloads</span>
<span class="arch-panel-extension-panel-dot"></span>
<span>Total (ACR): ${formatCurrency(appState.detailModal.totalAcr)}</span>`
}
</div>
${
isServiceRequestDetail
? renderServiceRequestDetailTable(detailItems)
: renderDetailTable(
detailItems,
appState.detailModal.scope === "calendar-day"
)
}
<div class="arch-panel-extension-detail-actions">
${
isServiceRequestDetail
? `<button
type="button"
class="arch-panel-extension-utility-button is-primary"
data-action="open-time-management"
>
⏱️ Manage time
</button>`
: ""
}
<button
type="button"
class="arch-panel-extension-close"
data-action="close-detail"
aria-label="Fechar detalhamento"
>
Fechar
</button>
</div>
</div>
</div>
`;
}
function renderActionFormModal() {
if (!appState.actionFormModal || !appState.dataset) {
return "";
}
const workload = findWorkloadById(appState.actionFormModal.workloadId);
const userEmail = appState.dataset.user?.userEmail || "";
if (!workload) {
return "";
}
const modalError = cleanString(appState.actionFormModal.errorMessage);
const isSubmitting = Boolean(appState.actionFormModal.isSubmitting);
return `
<div class="arch-panel-extension-detail-overlay">
<div class="arch-panel-extension-action-dialog" role="document">
<header class="arch-panel-extension-detail-head">
<div>
<p class="arch-panel-extension-panel-label">Create workload action</p>
<h3 class="arch-panel-extension-panel-title">Criar action</h3>
</div>
<button
type="button"
class="arch-panel-extension-close"
data-action="close-action-form"
aria-label="Fechar formulário"
>
Fechar
</button>
</header>
<div class="arch-panel-extension-detail-meta">
<span>${renderOpportunityLink(workload)}</span>
<span class="arch-panel-extension-panel-dot"></span>
<span>${escapeHtml(workload.customerName || "")}</span>
<span class="arch-panel-extension-panel-dot"></span>
<span>${escapeHtml(workload.name || "Unnamed workload")}</span>
</div>
${modalError ? `<p class="arch-panel-extension-form-error">${escapeHtml(modalError)}</p>` : ""}
<form class="arch-panel-extension-action-form">
<input type="hidden" name="parentType" value="WORKLOAD">
<input type="hidden" name="parentId" value="${escapeHtml(workload.workloadId || "")}">
<input type="hidden" name="ownerEmail" value="${escapeHtml(userEmail)}">
<input type="hidden" name="role" value="CLOUD_ARCHITECT">
<input type="hidden" name="name" value="SUCCESS_PLAN_CONSUM_RVW">
<input type="hidden" name="status" value="COMPLETE">
<input type="hidden" name="notes" value="[]">
<input type="hidden" name="objective" value="">
<input type="hidden" name="createdBy" value="${escapeHtml(userEmail)}">
<input type="hidden" name="updatedBy" value="${escapeHtml(userEmail)}">
<div class="arch-panel-extension-form-grid">
<label class="arch-panel-extension-form-field">
<span>Team</span>
<select name="team" required ${isSubmitting ? "disabled" : ""}>
<option value="">Selecione</option>
${ACTION_TEAM_OPTIONS.map(
(option) =>
`<option value="${escapeHtml(option.value)}">${escapeHtml(option.label)}</option>`
).join("")}
</select>
</label>
<label class="arch-panel-extension-form-field">
<span>Data de início</span>
<input type="date" name="startDate" required ${isSubmitting ? "disabled" : ""}>
</label>
<label class="arch-panel-extension-form-field">
<span>Data fim</span>
<input type="date" name="endDate" required ${isSubmitting ? "disabled" : ""}>
</label>
<label class="arch-panel-extension-form-field">
<span>Complexity</span>
<select name="complexity" required ${isSubmitting ? "disabled" : ""}>
<option value="">Selecione</option>
${ACTION_COMPLEXITY_OPTIONS.map(
(option) =>
`<option value="${escapeHtml(option)}">${escapeHtml(option)}</option>`
).join("")}
</select>
</label>
</div>
<div class="arch-panel-extension-detail-actions">
<button
type="button"
class="arch-panel-extension-close"
data-action="close-action-form"
aria-label="Fechar formulário"
${isSubmitting ? "disabled" : ""}
>
Fechar
</button>
<button
type="button"
class="arch-panel-extension-utility-button"
data-action="submit-action-form"
${isSubmitting ? "disabled" : ""}
>
${isSubmitting ? "Criando..." : "Criar action"}
</button>
</div>
</form>
</div>
</div>
`;
}
function renderRampComparisonModal() {
const modal = appState.rampComparisonModal;
if (!modal) {
return "";
}
const rows = modal.rows || [];
const title = `${modal.workload.customerName || "Unknown customer"} | ${
modal.workload.name || "Unnamed workload"
}${
modal.workload.description ? ` | ${modal.workload.description}` : ""
}`;
return `
<div class="arch-panel-extension-detail-overlay">
<div class="arch-panel-extension-ramp-dialog" role="document">
<header class="arch-panel-extension-detail-head">
<div>
<p class="arch-panel-extension-panel-label">Ramp forecast comparison</p>
<h3 class="arch-panel-extension-panel-title">${escapeHtml(title)}</h3>
</div>
</header>
<div class="arch-panel-extension-detail-meta">
<span>Ramp months: ${formatWholeNumber(modal.workload.rampMonths || 0)}</span>
<span class="arch-panel-extension-panel-dot"></span>
<span>Adjusted ACR: ${formatCurrency(modal.workload.adjustedACR)}</span>
<span class="arch-panel-extension-panel-dot"></span>
<span>Consumption start: ${escapeHtml(
formatDate(modal.workload.consumptionStartDate) || "-"
)}</span>
</div>
${renderRampComparisonPivotTable(rows)}
<div class="arch-panel-extension-detail-actions">
<button
type="button"
class="arch-panel-extension-utility-button is-primary"
data-action="open-forecast-update-confirm"
${rows.length ? "" : "disabled"}
>
Atualizar pelo Calculated ramp
</button>
<button
type="button"
class="arch-panel-extension-close"
data-action="close-ramp-comparison"
aria-label="Fechar comparação de rampa"
>
Fechar
</button>
</div>
</div>
</div>
`;
}
function renderRampComparisonPivotTable(rows) {
if (!rows.length) {
return `<div class="arch-panel-extension-empty-state">Nenhum forecast encontrado para comparar.</div>`;
}
return `
<div class="arch-panel-extension-table-wrap is-ramp-pivot">
<table class="arch-panel-extension-table arch-panel-extension-ramp-pivot-table">
<tbody>
<tr>
<th>Period</th>
${rows.map((row) => `<td>${escapeHtml(row.periodLabel)}</td>`).join("")}
</tr>
<tr>
<th>Current adjusted</th>
${rows
.map(
(row) =>
`<td class="is-numeric">${formatCurrency(row.currentValue)}</td>`
)
.join("")}
</tr>
<tr>
<th>Calculated ramp</th>
${rows
.map(
(row) =>
`<td class="is-numeric">${formatCurrency(row.calculatedValue)}</td>`
)
.join("")}
</tr>
<tr>
<th>Delta</th>
${rows
.map(
(row) =>
`<td class="is-numeric${
isNonZeroAmount(row.delta) ? " is-delta-different" : ""
}">${formatCurrency(row.delta)}</td>`
)
.join("")}
</tr>
</tbody>
</table>
</div>
`;
}
function renderForecastUpdateConfirmModal() {
const modal = appState.forecastUpdateConfirmModal;
if (!modal) {
return "";
}
return `
<div class="arch-panel-extension-detail-overlay is-confirmation">
<div class="arch-panel-extension-confirm-dialog" role="document">
<header class="arch-panel-extension-detail-head">
<div>
<p class="arch-panel-extension-panel-label">Confirmar atualização</p>
<h3 class="arch-panel-extension-panel-title">Atualizar forecast pelo Calculated ramp?</h3>
</div>
</header>
<p class="arch-panel-extension-detail-subtitle">
Esta ação enviará ${formatWholeNumber(
modal.payload.length
)} meses calculados para o workload atual. Os valores atuais do forecast serão substituídos pelos valores da linha Calculated ramp.
</p>
${
modal.errorMessage
? `<p class="arch-panel-extension-form-error">${escapeHtml(
modal.errorMessage
)}</p>`
: ""
}
<div class="arch-panel-extension-detail-actions">
<button
type="button"
class="arch-panel-extension-close"
data-action="close-forecast-update-confirm"
${modal.isSubmitting ? "disabled" : ""}
>
Cancelar
</button>
<button
type="button"
class="arch-panel-extension-utility-button is-primary"
data-action="submit-forecast-update"
${modal.isSubmitting ? "disabled" : ""}
>
${modal.isSubmitting ? "Atualizando..." : "Confirmar atualização"}
</button>
</div>
</div>
</div>
`;
}
function renderTimeEntriesDrawer() {
const drawer = appState.timeEntriesDrawer;
if (!drawer) {
return "";
}
const items = Array.isArray(drawer.items) ? drawer.items : [];
return `
<aside class="arch-panel-extension-time-drawer" aria-label="Time entries summary">
<header class="arch-panel-extension-time-drawer-head">
<div>
<p class="arch-panel-extension-panel-label">Horas apontadas</p>
<h3 class="arch-panel-extension-panel-title">SR ${escapeHtml(drawer.srNumber || "-")}</h3>
</div>
</header>
${renderTimeEntriesDrawerBody(drawer, items)}
<footer class="arch-panel-extension-time-drawer-actions">
<button
type="button"
class="arch-panel-extension-close arch-panel-extension-time-drawer-close"
data-action="close-time-entries"
aria-label="Fechar apontamentos de horas"
>
Fechar
</button>
</footer>
</aside>
`;
}
function renderTimeEntriesDrawerBody(drawer, items) {
if (drawer.status === "loading") {
return `
<div class="arch-panel-extension-time-drawer-state">
<p class="arch-panel-extension-summary-label">Carregando</p>
<p class="arch-panel-extension-summary-value">Buscando dados</p>
<p class="arch-panel-extension-summary-detail">Consultando o resumo de time entries na origem COMCIP.</p>
</div>
`;
}
if (drawer.status === "error") {
return `
<div class="arch-panel-extension-time-drawer-state is-error">
<p class="arch-panel-extension-summary-label">Erro na requisição</p>
<p class="arch-panel-extension-detail-subtitle">${escapeHtml(drawer.errorMessage || "Não foi possível carregar os apontamentos.")}</p>
<button type="button" class="arch-panel-extension-utility-button" data-action="retry-time-entries">
Tentar novamente
</button>
</div>
`;
}
const groupedItems = groupTimeEntriesByResourceName(items);
return `
<div class="arch-panel-extension-time-drawer-meta">
<span>${formatWholeNumber(items.length)} registros</span>
<span>${formatWholeNumber(groupedItems.length)} resources</span>
</div>
${
items.length
? groupedItems.map((group) => renderTimeEntryGroup(group)).join("")
: `<div class="arch-panel-extension-empty-state">Nenhum registro retornado para esta SR.</div>`
}
`;
}
function renderTimeEntryGroup(group) {
const totalTimeSpent = group.items.reduce((sum, item) => {
return sum + getTimeEntryTimeSpent(item);
}, 0);
return `
<section class="arch-panel-extension-time-entry-group">
<header class="arch-panel-extension-time-entry-group-head">
<div>
<p class="arch-panel-extension-summary-label">resourceName</p>
<h4>${escapeHtml(group.resourceName)}</h4>
</div>
<span>${formatHours(totalTimeSpent)} total</span>
</header>
<div class="arch-panel-extension-time-entry-table-wrap">
<table class="arch-panel-extension-time-entry-table">
<thead>
<tr>
<th>Activity</th>
<th>Task type</th>
<th class="is-numeric">Time spent</th>
</tr>
</thead>
<tbody>
${group.items.map((item) => renderTimeEntryRow(item)).join("")}
</tbody>
<tfoot>
<tr>
<th colspan="2">Total</th>
<th class="is-numeric">${formatHours(totalTimeSpent)}</th>
</tr>
</tfoot>
</table>
</div>
</section>
`;
}
function renderTimeEntryRow(item) {
return `
<tr>
<td>${escapeHtml(getTimeEntryActivity(item) || "-")}</td>
<td>${escapeHtml(getTimeEntryTaskType(item) || "-")}</td>
<td class="is-numeric">${formatHours(getTimeEntryTimeSpent(item))}</td>
</tr>
`;
}
function renderDetailTable(items, includeConsumptionStartDate = false) {
return `
<div class="arch-panel-extension-table-wrap is-detail">
<table class="arch-panel-extension-table">
<thead>
<tr>
<th>${renderSortButton("Opportunity", DETAIL_SORT_KEYS.opportunityNumber)}</th>
<th>${renderSortButton("Customer", DETAIL_SORT_KEYS.customerName)}</th>
<th>${renderSortButton("Workload", DETAIL_SORT_KEYS.workload)}</th>
${includeConsumptionStartDate ? "<th>consumptionStartDate</th>" : ""}
<th class="is-numeric">${renderSortButton("Ramp months", DETAIL_SORT_KEYS.rampMonths, true)}</th>
<th class="is-numeric">${renderSortButton("ACR", DETAIL_SORT_KEYS.adjustedACR, true)}</th>
<th>${renderSortButton("Type", DETAIL_SORT_KEYS.opportunityForecastTypeGroup)}</th>
<th>${renderSortButton("Tem SR?", DETAIL_SORT_KEYS.hasSR)}</th>
<th>${renderSortButton("Tem Action?", DETAIL_SORT_KEYS.hasAction)}</th>
</tr>
</thead>
<tbody>
${items
.map((workload) =>
renderWorkloadTableRow(workload, includeConsumptionStartDate, true)
)
.join("")}
</tbody>
</table>
</div>
`;
}
function renderTimeManagementModal() {
const modal = appState.timeManagementModal;
if (!modal) {
return "";
}
const selectedDate = modal.selectedDate || getDateInputValue(new Date());
return `
<div class="arch-panel-extension-detail-overlay">
<div class="arch-panel-extension-time-management-dialog" role="document">
<header class="arch-panel-extension-detail-head">
<div>
<p class="arch-panel-extension-panel-label">Horas apontadas</p>
<h3 class="arch-panel-extension-panel-title">Manage time</h3>
</div>
</header>
<div class="arch-panel-extension-time-management-toolbar">
<label class="arch-panel-extension-form-field">
<span>Selected date</span>
<span class="arch-panel-extension-date-picker">
<button
type="button"
class="arch-panel-extension-date-step-button"
data-action="time-management-date-week-prev"
aria-label="Previous week"
>
<svg viewBox="0 0 20 20" aria-hidden="true" focusable="false">
<path d="M12.78 4.22a.75.75 0 0 1 0 1.06L8.06 10l4.72 4.72a.75.75 0 1 1-1.06 1.06l-5.25-5.25a.75.75 0 0 1 0-1.06l5.25-5.25a.75.75 0 0 1 1.06 0Z"></path>
</svg>
</button>
<span class="arch-panel-extension-date-input-shell">
<button
type="button"
class="arch-panel-extension-date-picker-button"
data-action="open-time-management-date-picker"
aria-label="Open calendar"
>
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false">
<path d="M7 2a1 1 0 0 1 1 1v1h8V3a1 1 0 1 1 2 0v1h1.25A2.75 2.75 0 0 1 22 6.75v11.5A2.75 2.75 0 0 1 19.25 21H4.75A2.75 2.75 0 0 1 2 18.25V6.75A2.75 2.75 0 0 1 4.75 4H6V3a1 1 0 0 1 1-1ZM4.5 9.5v8.75c0 .69.56 1.25 1.25 1.25h12.5c.69 0 1.25-.56 1.25-1.25V9.5h-15Zm1.25-4c-.69 0-1.25.56-1.25 1.25V8h15V6.75c0-.69-.56-1.25-1.25-1.25H17v.5a1 1 0 1 1-2 0v-.5H9v.5a1 1 0 0 1-2 0v-.5H5.75Z"></path>
</svg>
</button>
<input
type="text"
value="${escapeHtml(selectedDate)}"
aria-label="Selected date"
readonly
>
${renderTimeManagementDatePicker(selectedDate)}
</span>
<button
type="button"
class="arch-panel-extension-date-step-button"
data-action="time-management-date-week-next"
aria-label="Next week"
>
<svg viewBox="0 0 20 20" aria-hidden="true" focusable="false">
<path d="M7.22 15.78a.75.75 0 0 1 0-1.06L11.94 10 7.22 5.28a.75.75 0 0 1 1.06-1.06l5.25 5.25a.75.75 0 0 1 0 1.06l-5.25 5.25a.75.75 0 0 1-1.06 0Z"></path>
</svg>
</button>
</span>
</label>
</div>
${renderTimeManagementBody(modal)}
<div class="arch-panel-extension-detail-actions">
<div class="arch-panel-extension-time-management-footer-actions">
<button
type="button"
class="arch-panel-extension-utility-button"
data-action="time-management-add-sr"
>
Add SR
</button>
<button
type="button"
class="arch-panel-extension-utility-button"
data-action="time-management-add-non-service"
>
Add (Non-service)
</button>
</div>
<button
type="button"
class="arch-panel-extension-close"
data-action="close-time-management"
aria-label="Fechar apontamento de horas"
>
Fechar
</button>
</div>
</div>
</div>
`;
}
function renderTimeManagementBody(modal) {
if (modal.status === "loading") {
return renderTimeManagementSkeleton();
}
if (modal.status === "error") {
return `
<div class="arch-panel-extension-time-management-card is-error">
<p class="arch-panel-extension-summary-label">Erro na requisição</p>
<p class="arch-panel-extension-detail-subtitle">${escapeHtml(
modal.errorMessage || "Não foi possível carregar o apontamento."
)}</p>
</div>
`;
}
const rows = getTimeManagementRows(modal);
return `
<div class="arch-panel-extension-time-management-card">
${renderTimeManagementHeaderRow(modal.weekDays || [])}
<div class="arch-panel-extension-time-management-rows">
${rows.map((row, index) => renderTimeManagementEntryRow(modal, row, index)).join("")}
</div>
</div>
`;
}
function renderTimeManagementHeaderRow(weekDays) {
return `
<div class="arch-panel-extension-time-management-header-row" aria-hidden="true">
<span class="arch-panel-extension-time-management-column-label">Service Request</span>
<span class="arch-panel-extension-time-management-column-label">Activity</span>
<span class="arch-panel-extension-time-management-column-label">Task Type</span>
${(weekDays || []).map((day) => renderTimeManagementDayHeader(day)).join("")}
<span class="arch-panel-extension-time-management-column-label" aria-label="Actions"></span>
</div>
`;
}
function renderTimeManagementDayHeader(day) {
return `
<span class="arch-panel-extension-time-management-day-head">
<strong>${escapeHtml(day.dayLabel || "-")}</strong>
<span>${escapeHtml(day.monthLabel || day.date || "-")}</span>
</span>
`;
}
function renderTimeManagementEntryRow(modal, row, index) {
const rowId = cleanString(row?.id) || `row-${index}`;
const selectedSr = cleanString(row?.selectedSrNumber);
const activityOptions = getTimeManagementActivityOptions(modal, selectedSr);
const selectedSrOption = findTimeManagementServiceRequestByNumber(
modal,
selectedSr
);
const isNonServiceRequest = isNonServiceRequestSrNumber(selectedSr);
const selectedSrLabel =
selectedSrOption?.label ||
formatTimeManagementServiceRequestLabel(selectedSrOption) ||
selectedSr;
const selectedActivityValue =
findOptionValue(activityOptions, row?.selectedActivityValue) ||
cleanString(row?.selectedActivityValue) ||
"";
const selectedActivityLabel =
findOptionLabel(activityOptions, selectedActivityValue) || selectedActivityValue;
const taskTypeOptions = getTimeManagementTaskTypeOptions(modal, selectedSr);
const selectedTaskTypeValue =
findOptionValue(taskTypeOptions, row?.selectedTaskTypeValue) ||
cleanString(row?.selectedTaskTypeValue) ||
"";
const selectedTaskTypeLabel =
findOptionLabel(taskTypeOptions, selectedTaskTypeValue) || selectedTaskTypeValue;
return `
<div class="arch-panel-extension-time-management-entry-row is-compact" data-row-id="${escapeHtml(rowId)}">
<label class="arch-panel-extension-form-field">
${renderSearchableCombobox({
field: "sr",
rowId,
selectedValue: selectedSr,
value: selectedSrLabel,
placeholder: "Search SR",
options: modal.serviceRequests || [],
valueKey: "srNumber",
labelKey: "label",
fallbackLabelKey: "srTitle",
disabled: isNonServiceRequest,
})}
</label>
<label class="arch-panel-extension-form-field">
${renderSearchableCombobox({
field: "activity",
rowId,
selectedValue: selectedActivityValue,
value: selectedActivityLabel,
placeholder: "Search activity",
options: activityOptions,
valueKey: "value",
labelKey: "label",
metaKey: "value",
disabled: isNonServiceRequest,
})}
</label>
<label class="arch-panel-extension-form-field">
${renderSearchableCombobox({
field: "taskType",
rowId,
selectedValue: selectedTaskTypeValue,
value: selectedTaskTypeLabel,
placeholder: "Search task type",
options: taskTypeOptions,
valueKey: "value",
labelKey: "label",
})}
</label>
${(modal.weekDays || [])
.map((day) =>
renderTimeManagementDayInput(day, rowId, row?.dayHours)
)
.join("")}
<button
type="button"
class="arch-panel-extension-time-management-delete"
data-action="time-management-remove-row"
data-row-id="${escapeHtml(rowId)}"
aria-label="Remover linha de apontamento"
title="Remover linha"
>
<svg viewBox="0 0 20 20" aria-hidden="true" focusable="false">
<path d="M7.5 2.5A1.5 1.5 0 0 1 9 1h2a1.5 1.5 0 0 1 1.5 1.5V3H16a.75.75 0 0 1 0 1.5h-.72l-.67 11.33A2.25 2.25 0 0 1 12.36 18H7.64a2.25 2.25 0 0 1-2.25-2.17L4.72 4.5H4A.75.75 0 0 1 4 3h3.5v-.5ZM9 3h2v-.5H9V3Zm-2.78 1.5.66 11.24c.02.42.36.76.76.76h4.72c.4 0 .74-.34.76-.76l.66-11.24H6.22Zm2.53 2.25c.41 0 .75.34.75.75v6a.75.75 0 0 1-1.5 0v-6c0-.41.34-.75.75-.75Zm3.25.75a.75.75 0 0 0-1.5 0v6a.75.75 0 0 0 1.5 0v-6Z"></path>
</svg>
</button>
</div>
`;
}
function renderTimeManagementSkeleton() {
return `
<div class="arch-panel-extension-time-management-card" aria-busy="true">
<div class="arch-panel-extension-time-management-header-row">
${["Service Request", "Activity", "Task Type"]
.map(
(label) =>
`<span class="arch-panel-extension-time-management-column-label">${label}</span>`
)
.join("")}
${Array.from({ length: 7 }, () => `
<span class="arch-panel-extension-time-management-day-head">
<span class="arch-panel-extension-skeleton arch-panel-extension-skeleton-title"></span>
<span class="arch-panel-extension-skeleton arch-panel-extension-skeleton-text"></span>
</span>
`).join("")}
<span></span>
</div>
<div class="arch-panel-extension-time-management-entry-row">
${Array.from({ length: 3 }, () => `
<div class="arch-panel-extension-form-field">
<span class="arch-panel-extension-skeleton arch-panel-extension-skeleton-control"></span>
</div>
`).join("")}
${Array.from({ length: 7 }, () => `
<div class="arch-panel-extension-time-management-day">
<span class="arch-panel-extension-skeleton arch-panel-extension-skeleton-input"></span>
</div>
`).join("")}
<span></span>
</div>
</div>
`;
}
function renderSearchableCombobox({
field,
rowId,
selectedValue,
value,
placeholder,
options,
valueKey,
labelKey,
fallbackLabelKey,
metaKey,
disabled = false,
}) {
const safeField = escapeHtml(field);
const safeRowId = escapeHtml(rowId || "");
const isDisabled = Boolean(disabled);
const normalizedSelectedValue = normalizeString(selectedValue);
const normalizedOptions = (options || [])
.map((item) => ({
value: cleanString(item?.[valueKey]),
label:
cleanString(item?.[labelKey]) ||
cleanString(item?.[fallbackLabelKey]) ||
cleanString(item?.[valueKey]),
meta: cleanString(item?.[metaKey]),
}))
.filter((item) => item.value || item.label);
return `
<span class="arch-panel-extension-combobox${isDisabled ? " is-disabled" : ""}" data-combobox-field="${safeField}" data-row-id="${safeRowId}">
<span class="arch-panel-extension-combobox-value" title="${escapeHtml(value || "")}">
${escapeHtml(value || placeholder || "")}
</span>
<input
type="search"
value=""
placeholder="${escapeHtml(placeholder || "")}"
data-combobox-field="${safeField}"
data-row-id="${safeRowId}"
autocomplete="off"
spellcheck="false"
role="combobox"
aria-autocomplete="list"
aria-expanded="false"
${isDisabled ? "disabled" : ""}
>
<span class="arch-panel-extension-combobox-arrow" aria-hidden="true">
<svg viewBox="0 0 20 20" focusable="false">
<path d="M5.23 7.21a.75.75 0 0 1 1.06.02L10 11.16l3.71-3.93a.75.75 0 1 1 1.08 1.04l-4.25 4.5a.75.75 0 0 1-1.08 0l-4.25-4.5a.75.75 0 0 1 .02-1.06Z"></path>
</svg>
</span>
<span class="arch-panel-extension-combobox-menu" role="listbox">
${
normalizedOptions.length
? normalizedOptions
.map(
(item, index) => {
const isSelected =
normalizedSelectedValue &&
normalizeString(item.value) === normalizedSelectedValue;
const meta =
item.meta && normalizeString(item.meta) !== normalizeString(item.label)
? item.meta
: "";
const optionId = `arch-panel-extension-combobox-${safeField}-${index}`;
return `
<button
id="${optionId}"
type="button"
class="arch-panel-extension-combobox-option${isSelected ? " is-selected" : ""}"
data-action="time-management-combobox-option"
data-field="${safeField}"
data-row-id="${safeRowId}"
data-value="${escapeHtml(item.value)}"
data-search="${escapeHtml(`${item.label} ${item.value}`.toLowerCase())}"
role="option"
aria-selected="${isSelected ? "true" : "false"}"
>
<span class="arch-panel-extension-combobox-option-main">
${escapeHtml(item.label || item.value)}
</span>
${
meta
? `<span class="arch-panel-extension-combobox-option-meta">${escapeHtml(meta)}</span>`
: ""
}
</button>
`;
}
)
.join("")
: `<span class="arch-panel-extension-combobox-empty">No options</span>`
}
</span>
</span>
`;
}
function renderTimeManagementDatePicker(selectedDate) {
const picker = appState.timeManagementDatePicker;
if (!picker.isOpen) {
return "";
}
const monthStart = new Date(picker.year, picker.month, 1);
const calendarStart = getWeekStartDate(monthStart);
const selectedDateKey = getLocalDateKey(selectedDate);
const todayKey = getLocalDateKey(new Date());
const monthLabel = new Intl.DateTimeFormat("en-US", {
month: "long",
year: "numeric",
}).format(monthStart);
const days = Array.from({ length: 42 }, (_, index) => {
const date = new Date(calendarStart);
date.setDate(calendarStart.getDate() + index);
const dateKey = getLocalDateKey(date);
const className = [
"arch-panel-extension-date-picker-day",
date.getMonth() === picker.month ? "" : "is-muted",
dateKey === todayKey ? "is-today" : "",
dateKey === selectedDateKey ? "is-selected" : "",
]
.filter(Boolean)
.join(" ");
return `
<button
type="button"
class="${className}"
data-action="time-management-date-picker-select"
data-date-value="${escapeHtml(dateKey)}"
>
${date.getDate()}
</button>
`;
});
return `
<div class="arch-panel-extension-date-picker-popover">
<div class="arch-panel-extension-date-picker-head">
<button
type="button"
class="arch-panel-extension-calendar-nav"
data-action="time-management-date-picker-prev"
aria-label="Previous month"
>
</button>
<strong>${escapeHtml(monthLabel)}</strong>
<button
type="button"
class="arch-panel-extension-calendar-nav"
data-action="time-management-date-picker-next"
aria-label="Next month"
>
</button>
</div>
<div class="arch-panel-extension-date-picker-weekdays">
${["S", "M", "T", "W", "T", "F", "S"].map((day) => `<span>${day}</span>`).join("")}
</div>
<div class="arch-panel-extension-date-picker-grid">
${days.join("")}
</div>
</div>
`;
}
function renderTimeManagementDayInput(
day,
rowId = "",
dayHours = {}
) {
const dateKey = getTimeManagementDayKey(day);
const value = cleanString(dayHours?.[dateKey]);
return `
<label class="arch-panel-extension-time-management-day">
<input
type="number"
min="0"
step="1"
value="${escapeHtml(value)}"
placeholder="0"
data-action="time-management-hours-change"
data-row-id="${escapeHtml(rowId || "")}"
data-date-key="${escapeHtml(dateKey)}"
aria-label="Horas apontadas em ${escapeHtml(day.date || "")}"
>
</label>
`;
}
function getTimeManagementDayKey(day) {
const parsedDate = parseTimeEntryDate(day?.date);
if (!Number.isNaN(parsedDate.getTime())) {
return getLocalDateKey(parsedDate);
}
return cleanString(day?.date);
}
function renderSelectOptions(items, selectedValue, valueKey, labelKey, emptyLabel) {
const options = [];
if (!items.length) {
options.push(`<option value="">${escapeHtml(emptyLabel)}</option>`);
}
return options
.concat(
items.map((item) => {
const value = cleanString(item?.[valueKey]);
const label = cleanString(item?.[labelKey]) || value || emptyLabel;
return `
<option value="${escapeHtml(value)}" ${
value === selectedValue ? "selected" : ""
}>${escapeHtml(label)}</option>
`;
})
)
.join("");
}
function buildRampForecastComparisonRows(workloads) {
return (workloads || [])
.flatMap((workload) => {
const startDate = parseDatePreservingDateOnly(workload.consumptionStartDate);
if (Number.isNaN(startDate.getTime())) {
return [];
}
const startMonth = startDate.getMonth() + 1;
const startYear = startDate.getFullYear();
const workloadLabel = `${workload.name || "Unnamed workload"}${
workload.description ? ` | ${workload.description}` : ""
}`;
return (workload.forecast || [])
.map((forecastItem) => {
const forecastMonth = parseForecastMonthNumber(forecastItem.month);
const forecastYear = toNumber(forecastItem.year);
if (!forecastMonth || !forecastYear) {
return null;
}
const monthIndex =
(forecastYear - startYear) * 12 + (forecastMonth - startMonth) + 1;
const currentValue = toNumber(forecastItem.adjustedConsumptionAmount);
const calculatedValue =
monthIndex >= 1 && monthIndex <= 12
? calculateRampForecastAmount({
adjustedACR: workload.adjustedACR,
rampMonths: workload.rampMonths,
monthIndex,
consumptionStartDate: startDate,
})
: 0;
return {
workloadLabel,
periodLabel: formatForecastPeriodLabel(forecastYear, forecastMonth),
currentValue,
calculatedValue,
delta: currentValue - calculatedValue,
monthIndex,
year: forecastYear,
month: forecastMonth,
sortKey: `${workloadLabel}|${String(forecastYear).padStart(
4,
"0"
)}-${String(forecastMonth).padStart(2, "0")}`,
};
})
.filter(Boolean);
})
.sort((left, right) => left.sortKey.localeCompare(right.sortKey));
}
function hasRampForecastMismatch(workload) {
return buildRampForecastComparisonRows([workload]).some((row) =>
isNonZeroAmount(row.delta)
);
}
function calculateRampForecastAmount({
adjustedACR,
rampMonths,
monthIndex,
consumptionStartDate,
}) {
const annualValue = toNumber(adjustedACR);
const ramp = Math.max(0, toNumber(rampMonths));
const month = Math.max(1, toNumber(monthIndex));
if (!annualValue) {
return 0;
}
if (ramp <= 0) {
return Math.round(
(annualValue / 12) *
getStartMonthProrationFactor(month, consumptionStartDate)
);
}
const steadyStateMonthlyAmount = (2 * annualValue) / (24 - ramp);
const rampStepCount = ramp + 1;
const calculatedAmount =
month <= rampStepCount
? steadyStateMonthlyAmount * (month / rampStepCount)
: steadyStateMonthlyAmount;
return Math.round(
calculatedAmount * getStartMonthProrationFactor(month, consumptionStartDate)
);
}
function getStartMonthProrationFactor(monthIndex, consumptionStartDate) {
if (toNumber(monthIndex) !== 1) {
return 1;
}
const startDate = parseDatePreservingDateOnly(consumptionStartDate);
if (Number.isNaN(startDate.getTime())) {
return 1;
}
const daysInMonth = new Date(
startDate.getFullYear(),
startDate.getMonth() + 1,
0
).getDate();
const remainingDays = daysInMonth - startDate.getDate() + 1;
return Math.max(0, Math.min(1, remainingDays / daysInMonth));
}
function isNonZeroAmount(value) {
return Math.abs(toNumber(value)) > 0.000001;
}
function parseForecastMonthNumber(value) {
if (typeof value === "number") {
return value >= 1 && value <= 12 ? value : 0;
}
const normalized = normalizeString(value);
const numericMonth = Number.parseInt(normalized, 10);
if (numericMonth >= 1 && numericMonth <= 12) {
return numericMonth;
}
const monthMap = {
JAN: 1,
JANUARY: 1,
FEB: 2,
FEBRUARY: 2,
MAR: 3,
MARCH: 3,
APR: 4,
APRIL: 4,
MAY: 5,
JUN: 6,
JUNE: 6,
JUL: 7,
JULY: 7,
AUG: 8,
AUGUST: 8,
SEP: 9,
SEPT: 9,
SEPTEMBER: 9,
OCT: 10,
OCTOBER: 10,
NOV: 11,
NOVEMBER: 11,
DEC: 12,
DECEMBER: 12,
};
return monthMap[normalized] || 0;
}
function formatForecastPeriodLabel(year, month) {
const fiscalYear = month >= 6 ? year + 1 : year;
const fiscalYearLabel = formatWeekNumber(fiscalYear % 100);
const monthLabel = new Intl.DateTimeFormat("en-US", {
month: "short",
}).format(new Date(year, month - 1, 1));
return `FY${fiscalYearLabel} ${monthLabel}`;
}
function renderServiceRequestDetailTable(items) {
return `
<div class="arch-panel-extension-table-wrap is-detail">
<table class="arch-panel-extension-table arch-panel-extension-sr-detail-table">
<thead>
<tr>
<th>Customer</th>
<th>SR Number</th>
<th>Queue</th>
<th>Service</th>
<th>Pillar</th>
<th class="is-numeric">Oppty value</th>
<th class="is-numeric">Horas apontadas</th>
<th>Customer deadline</th>
<th>Oppty Status</th>
</tr>
</thead>
<tbody>
${items
.map(
(serviceRequest) => `
<tr>
<td>${escapeHtml(serviceRequest.customerName || "-")}</td>
<td>${renderServiceRequestLink(serviceRequest)}</td>
<td>${escapeHtml(serviceRequest.queueName || "-")}</td>
<td>${escapeHtml(serviceRequest.serviceName || "-")}</td>
<td>${escapeHtml(serviceRequest.pillarLabel || "-")}</td>
<td class="is-numeric">${formatCurrency(serviceRequest.opportunityValue)}</td>
<td class="is-numeric">${renderReportedHoursLink(serviceRequest)}</td>
<td>${escapeHtml(formatDate(serviceRequest.customerDeadline) || "-")}</td>
<td>${escapeHtml(serviceRequest.opportunityStatusLabel || "-")}</td>
</tr>
`
)
.join("")}
</tbody>
</table>
</div>
`;
}
function renderSortButton(label, sortKey, isNumeric = false) {
const currentKey = appState.detailModal?.sortKey || "";
const currentDirection = appState.detailModal?.sortDirection || "asc";
const indicator =
currentKey === sortKey ? (currentDirection === "asc" ? " ↑" : " ↓") : "";
return `
<button
type="button"
class="arch-panel-extension-sort-button${isNumeric ? " is-numeric" : ""}"
data-action="sort-detail"
data-sort-key="${escapeHtml(sortKey)}"
aria-label="Sort by ${escapeHtml(label)}"
>
<span>${escapeHtml(label)}${indicator}</span>
</button>
`;
}
function renderOpportunityLink(workload) {
const label = workload.opportunityNumber || "Opportunity";
if (!workload.opportunityId || !workload.opportunityNumber) {
return escapeHtml(label);
}
const href = `https://eeho.fa.us2.oraclecloud.com/fscmUI/redwood/cx-sales/application/container/opportunities/opportunities-detail?id=${encodeURIComponent(
workload.opportunityId
)}&puid=${encodeURIComponent(workload.opportunityNumber)}`;
return `<a class="arch-panel-extension-row-link" href="${href}" target="_blank" rel="noreferrer">${escapeHtml(label)}</a>`;
}
function renderServiceRequestLink(serviceRequest) {
const srNumber = cleanString(serviceRequest?.srNumber);
if (!srNumber) {
return "-";
}
const href = `https://eeho.fa.us2.oraclecloud.com/fscmUI/redwood/service/ec/container/sr/edit?srNumber=${encodeURIComponent(
srNumber
)}`;
return `<a class="arch-panel-extension-row-link" href="${href}" target="_blank" rel="noreferrer">${escapeHtml(srNumber)}</a>`;
}
function renderReportedHoursLink(serviceRequest) {
const srNumber = cleanString(serviceRequest?.srNumber);
const label = formatHours(serviceRequest?.totalHoursWorked);
if (!srNumber) {
return escapeHtml(label);
}
return `
<button
type="button"
class="arch-panel-extension-row-link arch-panel-extension-inline-link-button"
data-action="open-time-entries"
data-sr-number="${escapeHtml(srNumber)}"
aria-label="Abrir apontamentos de horas da SR ${escapeHtml(srNumber)}"
>
${escapeHtml(label)}
</button>
`;
}
function renderWorkloadLink(workload, showRampAlert = false) {
const description = workload.description ? ` | ${workload.description}` : "";
const label = `${workload.name || "Unnamed workload"}${description}`;
if (!workload.workloadId) {
return `${escapeHtml(label)}${renderRampAlertButton(workload, showRampAlert)}`;
}
const href = `https://spa.oracle.com/oalcrm/web/api/g2m-consumer-application/ui/index.html?ojr=workload_workbench/workload_details;workloadId=${encodeURIComponent(
workload.workloadId
)}`;
return `
<span class="arch-panel-extension-workload-link-cell">
<a class="arch-panel-extension-row-link" href="${href}" target="_blank" rel="noreferrer">${escapeHtml(label)}</a>
${renderRampAlertButton(workload, showRampAlert)}
</span>
`;
}
function renderRampAlertButton(workload, showRampAlert) {
if (!showRampAlert || !workload.workloadId) {
return "";
}
return `
<button
type="button"
class="arch-panel-extension-ramp-alert-button"
data-action="open-ramp-comparison"
data-workload-id="${escapeHtml(workload.workloadId || "")}"
title="Requer atenção: existe Delta diferente de zero"
aria-label="Abrir comparação de rampa com inconsistência para ${escapeHtml(
workload.name || "workload"
)}"
>
!
</button>
`;
}
function getHeaderNote() {
if (appState.status === "loading") {
return appState.loadingMessage || "Loading";
}
if (appState.status === "error") {
return "Extraction failed";
}
if (appState.dataset) {
return `${appState.dataset.user.userEmail} · ${formatWholeNumber(
appState.dataset.summary.eligibleWorkloads.length
)} eligible workloads`;
}
return "Ready";
}
function getHeaderSummary() {
if (appState.status === "loading") {
return appState.loadingMessage || "Loading";
}
if (appState.status === "error") {
return "Extraction failed";
}
if (appState.dataset) {
return `${appState.dataset.user.userEmail} | ${formatWholeNumber(
appState.dataset.summary.eligibleWorkloads.length
)} eligible workloads`;
}
return "Ready";
}
function openModal() {
const overlay = ensureModal();
if (!overlay) {
return;
}
window.clearTimeout(closeTimer);
restoreFocusTarget =
document.activeElement instanceof HTMLElement ? document.activeElement : null;
overlay.hidden = false;
overlay.setAttribute("aria-hidden", "false");
lockScroll();
window.requestAnimationFrame(() => {
overlay.classList.add(CLASSES.overlayOpen);
const primaryAction = overlay.querySelector("[data-action='close']");
primaryAction?.focus();
});
if (!appState.dataset && appState.status !== "loading") {
void hydrateDatasetFromCache();
}
}
function closeModal() {
const overlay = document.getElementById(IDS.overlay);
if (!overlay || overlay.hidden) {
return;
}
overlay.classList.remove(CLASSES.overlayOpen);
overlay.setAttribute("aria-hidden", "true");
unlockScroll();
appState.detailModal = null;
appState.actionFormModal = null;
appState.rampComparisonModal = null;
appState.forecastUpdateConfirmModal = null;
appState.timeEntriesDrawer = null;
appState.timeManagementModal = null;
if (restoreFocusTarget && restoreFocusTarget.isConnected) {
restoreFocusTarget.focus();
}
restoreFocusTarget = null;
window.clearTimeout(closeTimer);
closeTimer = window.setTimeout(() => {
overlay.hidden = true;
}, 160);
}
async function refreshData() {
if (appState.status === "loading") {
return;
}
try {
appState.status = "loading";
appState.errorMessage = "";
updateLoadingProgress("Requesting current user", 4);
const user = await fetchCurrentUser();
updateLoadingProgress("Loading current resource user", 8);
const resourceCurrentUser = await fetchResourceCurrentUser(user.userEmail).catch(
() => null
);
updateLoadingProgress("Loading customer summary pages", 12);
const customers = await fetchAllCustomers(user.userEmail);
updateLoadingProgress(
`Loading workloads for ${formatWholeNumber(customers.length)} customers`,
24
);
let completedCustomers = 0;
const customersWithWorkloads = await mapWithConcurrency(
customers,
4,
async (customer, index) => {
updateLoadingProgress(
`Loading workloads ${index + 1}/${customers.length}: ${customer.name}`,
getSegmentProgress(24, 52, completedCustomers, customers.length)
);
const workloads = await fetchCustomerWorkloads(customer);
completedCustomers += 1;
updateLoadingProgress(
`Loaded workloads ${completedCustomers}/${customers.length}: ${customer.name}`,
getSegmentProgress(24, 52, completedCustomers, customers.length)
);
return {
customerId: customer.customerId,
name: customer.name,
workloads,
};
}
);
const allWorkloads = customersWithWorkloads.flatMap((customer) => {
return customer.workloads.map((workload) => ({
...workload,
customerId: customer.customerId,
customerName: customer.name,
}));
});
updateLoadingProgress(
`Loading actions and service requests for ${formatWholeNumber(
allWorkloads.length
)} workloads`,
56
);
let completedWorkloads = 0;
const enrichedWorkloads = await mapWithConcurrency(
allWorkloads,
6,
async (workload, index) => {
updateLoadingProgress(
`Loading actions and SRs ${index + 1}/${allWorkloads.length}: ${workload.name}`,
getSegmentProgress(56, 86, completedWorkloads, allWorkloads.length)
);
const [actions, serviceRequests] = await Promise.all([
fetchWorkloadActions(workload.workloadId),
fetchWorkloadServiceRequests(workload.workloadId),
]);
completedWorkloads += 1;
updateLoadingProgress(
`Loaded actions and SRs ${completedWorkloads}/${allWorkloads.length}: ${workload.name}`,
getSegmentProgress(56, 86, completedWorkloads, allWorkloads.length)
);
const hasSR = serviceRequests.some((serviceRequest) => {
return (
normalizeString(serviceRequest.status) === "SVC_PENDING_DELIVERY" &&
teamContainsUser(serviceRequest.team, user.userEmail)
);
});
const hasAction = actions.some((action) => {
return normalizeString(action.owner) === normalizeString(user.userEmail);
});
return {
...workload,
actions,
serviceRequests,
hasSR,
hasAction,
};
}
);
const customersMap = new Map(
customersWithWorkloads.map((customer) => [
customer.customerId,
{
customerId: customer.customerId,
name: customer.name,
workloads: [],
},
])
);
for (const workload of enrichedWorkloads) {
const customer = customersMap.get(workload.customerId);
if (customer) {
customer.workloads.push(stripTransientFields(workload));
}
}
updateLoadingProgress("Loading pending delivery service requests", 90);
const pendingServiceRequests = await fetchPendingServiceRequests().catch(
() => []
);
updateLoadingProgress("Refreshing time management task types", 94);
await refreshTaskTypeCache().catch(() => null);
updateLoadingProgress("Preparing dashboard cache", 96);
const finalCustomers = Array.from(customersMap.values());
const exportPayload = createExportPayload(
user,
finalCustomers,
pendingServiceRequests,
resourceCurrentUser
);
const dataset = createDatasetSnapshot(exportPayload);
await writeCachedDataset(exportPayload);
updateLoadingProgress("Dashboard ready", 100);
appState.dataset = dataset;
appState.status = "ready";
appState.loadingMessage = "";
appState.loadingProgress = 0;
appState.detailModal = null;
renderModal();
} catch (error) {
appState.status = "error";
appState.errorMessage = getErrorMessage(error);
appState.loadingMessage = "";
appState.loadingProgress = 0;
renderModal();
}
}
function updateLoadingProgress(message, progress) {
appState.loadingMessage = message;
appState.loadingProgress = Math.max(0, Math.min(Number(progress) || 0, 100));
if (!updateLoadingProgressDom()) {
renderModal();
window.requestAnimationFrame(() => {
updateLoadingProgressDom();
});
}
}
function updateLoadingProgressDom() {
const overlay = document.getElementById(IDS.overlay);
const progress = Math.max(0, Math.min(appState.loadingProgress || 0, 100));
const stateCopy = overlay?.querySelector(".arch-panel-extension-state-copy");
const progressRoot = overlay?.querySelector(
".arch-panel-extension-loading-progress"
);
const progressBar = overlay?.querySelector(
".arch-panel-extension-loading-progress-fill"
);
const progressTrackValue = overlay?.querySelector(
".arch-panel-extension-loading-progress-value"
);
const shellNote = overlay?.querySelector(".arch-panel-extension-shell-note");
if (!stateCopy || !progressRoot || !progressBar) {
return false;
}
stateCopy.textContent = appState.loadingMessage;
progressRoot.setAttribute("aria-valuenow", String(Math.round(progress)));
progressBar.style.width = `${progress}%`;
if (progressTrackValue) {
progressTrackValue.textContent = formatProgressPercent(progress);
}
if (shellNote) {
shellNote.textContent = appState.loadingMessage || "Loading";
}
return true;
}
function getSegmentProgress(start, end, current, total) {
const denominator = Math.max(Number(total) || 0, 1);
const ratio = Math.max(0, Math.min((Number(current) || 0) / denominator, 1));
return start + (end - start) * ratio;
}
async function fetchPendingServiceRequests() {
const response = await sendComcipPostMessage();
if (!response?.ok) {
const payloadMessage =
typeof response?.payload === "string"
? cleanString(response.payload)
: firstNonEmptyString([
response?.payload?.message,
response?.payload?.error,
response?.payload?.title,
response?.payload?.detail,
]);
throw new Error(
payloadMessage ||
response?.error ||
`COMCIP request failed (${response?.status || 0}).`
);
}
return normalizePendingServiceRequests(response.payload);
}
function sendComcipPostMessage() {
return sendComcipRequestMessage({
method: "POST",
url: COMCIP_REQUEST.url,
payload: createComcipServiceRequestPayload(),
timeoutMessage: "Timed out while requesting COMCIP data.",
});
}
function sendComcipGetMessage(url) {
return sendComcipRequestMessage({
method: "GET",
url,
payload: null,
timeoutMessage: "Timed out while requesting COMCIP data.",
});
}
async function sendComcipRequestMessage({ method, url, payload, timeoutMessage }) {
const useTimeEntryFrame = isComcipTimeEntryUrl(url);
if (useTimeEntryFrame) {
await ensureComcipTimeEntryFrame();
}
return new Promise((resolve, reject) => {
const timeout = window.setTimeout(() => {
reject(new Error(timeoutMessage || "Timed out while requesting COMCIP data."));
}, 130000);
chrome.runtime.sendMessage(
{
type: "ARCH_PANEL_COMCIP_REQUEST",
method,
url,
payload,
useTimeEntryFrame,
headers: {
"accept-language": navigator.language || "pt-BR",
"x-vb-application-version": COMCIP_REQUEST.appVersion,
},
},
(response) => {
window.clearTimeout(timeout);
const runtimeError = chrome.runtime.lastError;
if (runtimeError) {
reject(new Error(runtimeError.message));
return;
}
resolve(response || null);
}
);
});
}
function isComcipTimeEntryUrl(url) {
return String(url || "").includes("/ic/builder/rt/oalset_timeentrymobile/live");
}
function ensureComcipTimeEntryFrame() {
const existingFrame = document.getElementById(COMCIP_TIME_ENTRY_FRAME_ID);
if (existingFrame instanceof HTMLIFrameElement) {
existingFrame.name = COMCIP_TIME_ENTRY_FRAME_ID;
return Promise.resolve(existingFrame);
}
return new Promise((resolve, reject) => {
const frame = document.createElement("iframe");
const timeout = window.setTimeout(() => {
reject(new Error("Timed out while preparing COMCIP Time Entry session frame."));
}, 45000);
frame.id = COMCIP_TIME_ENTRY_FRAME_ID;
frame.name = COMCIP_TIME_ENTRY_FRAME_ID;
frame.title = "COMCIP Time Entry session";
frame.src = COMCIP_REQUEST.timeEntryAppUrl;
frame.setAttribute("aria-hidden", "true");
frame.tabIndex = -1;
frame.style.position = "fixed";
frame.style.width = "1px";
frame.style.height = "1px";
frame.style.opacity = "0";
frame.style.pointerEvents = "none";
frame.style.border = "0";
frame.style.left = "-10000px";
frame.style.top = "0";
frame.addEventListener(
"load",
() => {
window.clearTimeout(timeout);
resolve(frame);
},
{ once: true }
);
frame.addEventListener(
"error",
() => {
window.clearTimeout(timeout);
frame.remove();
reject(new Error("Unable to load COMCIP Time Entry session frame."));
},
{ once: true }
);
document.documentElement.appendChild(frame);
});
}
function createComcipServiceRequestPayload() {
return {
entity: "CRMServiceRequest",
offset: 0,
limit: 100,
onlyData: true,
language: "US",
fields: [
"SrNumber",
"Title",
"LastUpdateDate",
"CreationDate",
"PrimaryPillarNew_c",
"PrimaryPillarNew_c_localizedValue",
"CountryText_c",
"OpportunityName_Id_c.OptyId",
"OpportunityName_Id_c.Name",
"OpportunityName_Id_c.OptyNumber",
"OpportunityName_Id_c.OpportunityType_c",
"OpportunityName_Id_c.StatusCode",
"OpportunityName_Id_c.EffectiveDate",
"OpportunityName_Id_c.StatusCode_localizedValue",
"OpportunityName_Id_c.PrimaryRevenue.WinProb",
"OpportunityName_Id_c.PrimaryRevenue.RevnAmount",
"OpportunityName_Id_c.PrimaryRevenue.CrmConversionRate",
"OpportunityName_Id_c.WorkloadAmountUSDWorkspace_c",
"CustomerAccount.PartyId",
"CustomerAccount.PartyUniqueName",
"CustomerAccount.PartyNumber",
"CustomerAccount.NamedFlag",
"StatusCd",
"StatusCd_localizedValue",
"ServiceNew_Id_c",
"CustomerDeadline_c",
"Owner.PartyId",
"Owner.PartyName",
"Owner.PrimaryEmail.EmailAddress",
"Owner.ResourceEmailAddress",
"ReportedByPartyId.PrimaryEmail.EmailAddress",
"Queue.QueueId",
"Queue.QueueName",
"ServiceRequest_Id_SRToInternalSR",
"ReportedHours_c",
"ProblemDescription",
"AccountRegistryId_c",
],
sort: [
{
attribute: "LastUpdateDate",
direction: "descending",
},
],
q: {
op: "$and",
criteria: [
{
op: "$eq",
attribute: "RecordSet",
value: "ORA_SVC_MY_SUBORD_SR_TEAM_SRS",
},
{
op: "$in",
attribute: "StatusCd",
values: ["SVC_PENDING_DELIVERY"],
},
],
},
aggregations: {
StatusCd: {
ignore: true,
terms: {
attribute: "StatusCd",
includeTerms: [
"SVC_PENDING_ACCEPTANCE",
"SVC_PENDING_DELIVERY",
"SVC_PENDING_RESOURCING",
"SVC_SE_MGR_CLOSED",
"SVC_REJECT",
],
excludeTerms: ["SVC_PENDING_DISPATCHER", "ORA_SVC_NEW", "SVC_REJECT"],
minBucketCount: 1,
maxNumberOfBuckets: 5,
other: false,
missing: false,
localize: true,
},
},
Queue: createTermsAggregation("Queue", 10),
CustomerAccount: createTermsAggregation("CustomerAccount", 5),
OpportunityStatus: createTermsAggregation(
"OpportunityName_Id_c.StatusCode",
6
),
Service: createTermsAggregation("ServiceNew_Id_c", 5),
Country: createTermsAggregation("CountryText_c", 5),
Pillar: createTermsAggregation("PrimaryPillarNew_c", 5),
WinProb: {
ignore: true,
range: {
attribute: "OpportunityName_Id_c.PrimaryRevenue.WinProb",
ranges: [
{ key: "0-10 % SQL", from: 0, to: 11 },
{ key: "20-30 % Pipeline", from: 20, to: 31 },
{ key: "40-50 % Upside", from: 40, to: 51 },
{ key: "60-90 % Forecast", from: 60, to: 91 },
{ key: "100 % Won", from: 100, to: 101 },
],
},
},
Amount: {
ignore: true,
range: {
attribute: "OpportunityName_Id_c.PrimaryRevenue.RevnAmount",
ranges: [
{ key: "0-10,000", from: 0, to: 10000 },
{ key: "10,000-50,000", from: 10000, to: 50000 },
{ key: "50,000-100,000", from: 50000, to: 100000 },
{ key: ">=100,000", from: 100000 },
],
},
},
OpportunityName_Id_c: createTermsAggregation("OpportunityName_Id_c", 5),
},
};
}
function createTermsAggregation(attribute, maxNumberOfBuckets) {
return {
ignore: true,
terms: {
attribute,
includeTerms: [],
minBucketCount: 1,
maxNumberOfBuckets,
other: false,
missing: false,
localize: true,
},
};
}
function normalizePendingServiceRequests(payload) {
const items = Array.isArray(payload)
? payload
: extractArray(payload, ["items", "data", "results", "content"]);
return items
.map((item) => normalizePendingServiceRequest(item))
.filter((item) => item.srNumber || item.customerName || item.lastUpdateDate)
.sort((left, right) => {
return (
new Date(right.lastUpdateDate || 0).getTime() -
new Date(left.lastUpdateDate || 0).getTime()
);
});
}
function normalizePendingServiceRequest(item) {
return {
customerName: cleanString(
getFieldValue(item, [
"CustomerAccount.PartyUniqueName",
"CustomerAccount.PartyName",
"CustomerAccount.Name",
"customerName",
])
),
lastUpdateDate: cleanString(getFieldValue(item, ["LastUpdateDate", "lastUpdateDate"])),
srNumber: cleanString(getFieldValue(item, ["SrNumber", "srNumber"])),
queueName: cleanString(
getFieldValue(item, ["Queue.QueueName", "QueueName", "queueName"])
),
requestedBy: cleanString(
getFieldValue(item, [
"ReportedByPartyId.PrimaryEmail.EmailAddress",
"ReportedByPartyId.EmailAddress",
"requestedBy",
])
),
creationDate: cleanString(getFieldValue(item, ["CreationDate", "creationDate"])),
opportunityNumber: cleanString(
getFieldValue(item, [
"OpportunityName_Id_c.OptyNumber",
"OptyNumber",
"opportunityNumber",
])
),
srStatusLabel: cleanString(
getFieldValue(item, ["StatusCd_localizedValue", "StatusCd", "srStatusLabel"])
),
opportunityStatusLabel: cleanString(
getFieldValue(item, [
"OpportunityName_Id_c.StatusCode_localizedValue",
"OpportunityName_Id_c.StatusCode",
"opportunityStatusLabel",
])
),
serviceName: cleanString(
getFieldValue(item, [
"ServiceNew_Id_c.RecordName",
"ServiceNew_Id_c",
"serviceName",
])
),
country: cleanString(getFieldValue(item, ["CountryText_c", "country"])),
totalHoursWorked: toNumber(
getFieldValue(item, ["ReportedHours_c", "totalHoursWorked"])
),
customerDeadline: cleanString(
getFieldValue(item, ["CustomerDeadline_c", "customerDeadline"])
),
opportunityValue: toNumber(
getFieldValue(item, [
"WorkloadAmountUSDWorkspace_c",
"OpportunityName_Id_c.WorkloadAmountUSDWorkspace_c",
"OpportunityName_Id_c.PrimaryRevenue.RevnAmount",
"opportunityValue",
])
),
pillarLabel: cleanString(
getFieldValue(item, [
"PrimaryPillarNew_c_localizedValue",
"PrimaryPillarNew_c",
"pillarLabel",
])
),
};
}
function getFieldValue(source, paths) {
if (!source || typeof source !== "object") {
return "";
}
for (const path of paths) {
if (Object.prototype.hasOwnProperty.call(source, path)) {
return source[path];
}
const parts = String(path).split(".");
let current = source;
for (const part of parts) {
if (!current || typeof current !== "object") {
current = "";
break;
}
current = current[part];
}
if (current !== undefined && current !== null && current !== "") {
return current;
}
}
return "";
}
async function hydrateDatasetFromCache() {
if (appState.dataset || cacheHydrationPromise) {
return cacheHydrationPromise;
}
appState.status = "loading";
appState.errorMessage = "";
appState.loadingMessage = "Loading cached snapshot";
appState.loadingProgress = 18;
renderModal();
cacheHydrationPromise = (async () => {
try {
const exportPayload = await readCachedDataset();
appState.loadingProgress = exportPayload ? 100 : 0;
if (exportPayload) {
appState.dataset = createDatasetSnapshot(exportPayload);
appState.status = "ready";
} else {
appState.status = "idle";
}
} catch (error) {
appState.status = "error";
appState.errorMessage = getErrorMessage(error);
} finally {
appState.loadingMessage = "";
appState.loadingProgress = 0;
cacheHydrationPromise = null;
renderModal();
}
})();
return cacheHydrationPromise;
}
async function fetchCurrentUser() {
const currentFrameToken = syncWorklistIframeToken({ force: true })?.token;
const worklistToken = currentFrameToken || (await readCachedWorklistToken());
if (!worklistToken) {
throw new Error(
"Unable to resolve Worklist token. Reload the FuseWelcome page so the Worklist iframe token can be captured before refreshing data."
);
}
const url = new URL(API_ENDPOINTS.worklistCurrentUser);
url.search = new URLSearchParams({
source: "saasui",
_: String(Date.now()),
token: worklistToken,
}).toString();
appState.sessionAuthHeaders = {};
const payload = await fetchJson(url.toString(), { forceExtension: true });
const userPayload = normalizeWorklistUserPayload(payload);
const firstName = cleanString(
getFieldValue(userPayload, ["FirstName", "firstName", "User.FirstName"])
);
const lastName = cleanString(
getFieldValue(userPayload, ["LastName", "lastName", "User.LastName"])
);
const userEmail = cleanString(
getFieldValue(userPayload, [
"Email",
"email",
"EmailAddress",
"emailAddress",
"User.Email",
"User.email",
"User.EmailAddress",
])
);
const displayName = firstNonEmptyString([
`${firstName} ${lastName}`.trim(),
userPayload?.DisplayName,
userPayload?.displayName,
userEmail,
]);
if (!userEmail) {
const userKeys =
userPayload && typeof userPayload === "object"
? Object.keys(userPayload).join(", ")
: "";
throw new Error(
`Unable to resolve userEmail from Worklist user response. Available top-level keys: ${Object.keys(
payload || {}
).join(", ")}${userKeys ? ` | User keys: ${userKeys}` : ""}`
);
}
appState.sessionAuthHeaders = {};
return {
userEmail,
name: displayName || userEmail,
avatar: cleanString(getFieldValue(userPayload, ["ImageURL", "imageUrl", "User.ImageURL"])),
administrator: Boolean(
getFieldValue(userPayload, ["Administrator", "administrator", "User.Administrator"])
),
};
}
function normalizeWorklistUserPayload(payload) {
const candidate = payload?.User ?? payload?.user ?? payload;
if (Array.isArray(candidate)) {
return candidate[0] || {};
}
if (candidate?.User) {
return normalizeWorklistUserPayload(candidate);
}
return candidate || {};
}
async function fetchResourceCurrentUser(userEmail) {
const normalizedUserEmail = cleanString(userEmail);
if (!normalizedUserEmail) {
throw new Error("Unable to request resource current user without userEmail.");
}
const url = `${COMCIP_REQUEST.resourceCurrentUserUrl}?emailAddress=${encodeURIComponent(
normalizedUserEmail
)}`;
const response = await sendComcipGetMessage(url);
if (!response?.ok) {
const payloadMessage =
typeof response?.payload === "string"
? cleanString(response.payload)
: firstNonEmptyString([
response?.payload?.message,
response?.payload?.error,
response?.payload?.title,
response?.payload?.detail,
]);
throw new Error(
payloadMessage ||
response?.error ||
`COMCIP resource user request failed (${response?.status || 0}).`
);
}
return response.payload;
}
async function fetchAllCustomers(userEmail) {
const limit = 49;
let offset = 0;
const customers = [];
const seen = new Set();
while (true) {
const url = new URL(API_ENDPOINTS.customerSummary, TARGET_ORIGIN);
url.search = new URLSearchParams({
emailAddress: userEmail,
projectionPeriod: "ALL",
recordSet: "IAM_ON_SR_TEAM",
growth: "All",
projectedGrowth: "All",
attainmentBand: "All",
projectedConsumption: "All",
customerStatus: "All",
workloadStatus: "All",
lastProjectionUpdate: "All",
revenueForecastType: "All",
customerProgramCode: "All",
planType: "All",
workLoadLivePeriod: "All",
activeConsumptionOnly: "false",
favorite: "false",
workloadDeleted: "false",
awsFlag: "N",
googleFlag: "N",
azureFlag: "N",
excludePayAsYouGoOnly: "false",
projectionReview: "N",
pillarFilter: "dpOci",
sortBy: "CUSTOMER_NAME",
sortOrder: "ASC",
limit: String(limit),
offset: String(offset),
}).toString();
const payload = await fetchJson(url.toString());
const items = extractArray(payload, [
"items",
"customers",
"content",
"results",
"records",
"data",
]);
const mapped = items
.map((item) => ({
customerId: cleanString(item?.id),
name: item?.name || "",
}))
.filter((item) => item.customerId);
for (const item of mapped) {
if (!seen.has(item.customerId)) {
seen.add(item.customerId);
customers.push(item);
}
}
if (mapped.length < limit) {
break;
}
offset += limit;
}
return customers;
}
async function fetchCustomerWorkloads(customer) {
const url = new URL(
`${API_ENDPOINTS.customerWorkloads}/${encodeURIComponent(
customer.customerId
)}/workloads`,
TARGET_ORIGIN
);
url.search = new URLSearchParams({
workloadStatus: "Pipeline,Implementation,Live",
opportunityForecastType: "All",
includeForEstimates: "ALL",
financialYear: "ROLLING_4_QTRS",
}).toString();
const payload = await fetchJson(url.toString());
const items = extractArray(payload, [
"items",
"workloads",
"content",
"results",
"data",
]);
return items
.map((item) => normalizeWorkload(item))
.filter(Boolean)
.filter((workload) => !isWonWorkload(workload));
}
async function fetchWorkloadActions(workloadId) {
const url = new URL(API_ENDPOINTS.workloadActions, TARGET_ORIGIN);
url.search = new URLSearchParams({
parentType: "WORKLOAD",
parentId: workloadId,
}).toString();
const payload = await fetchJson(url.toString());
const items = extractArray(payload, ["items", "actions", "content", "results", "data"]);
return items.map((item) => ({
owner: item?.owner || "",
role: item?.role || "",
name: item?.name || "",
team: normalizeTeamValue(item?.team),
startDate: item?.startDate || "",
endDate: item?.endDate || "",
complexity: item?.complexity || "",
}));
}
async function fetchWorkloadServiceRequests(workloadId) {
const url = new URL(
`${API_ENDPOINTS.workloadServiceRequests}/${encodeURIComponent(
workloadId
)}/serviceRequests`,
TARGET_ORIGIN
);
const payload = await fetchJson(url.toString());
const items = extractArray(payload, [
"items",
"serviceRequests",
"content",
"results",
"data",
]);
return items.map((item) => ({
srNumber: item?.srNumber || "",
status: item?.status || "",
team: normalizeTeamValue(item?.team),
}));
}
async function fetchJson(url, options = {}) {
if (options.forceExtension) {
return fetchJsonViaExtension(url, options);
}
if (shouldUseSpaFrameFetch(url) && !options.forceExtension) {
return fetchJsonViaSpaFrame(url, options);
}
if (shouldUseExtensionFetch(url) && !options.forcePageBridge) {
return fetchJsonViaExtension(url, options);
}
await ensurePageBridge();
const requestId = `${Date.now()}-${++requestCounter}`;
const requestUrl = toBridgeRequestUrl(url);
const response = await new Promise((resolve, reject) => {
const timeout = window.setTimeout(() => {
pendingBridgeRequests.delete(requestId);
reject(new Error(`Timed out while requesting ${requestUrl}`));
}, 60000);
pendingBridgeRequests.set(requestId, {
resolve,
reject,
timeout,
});
window.postMessage(
{
source: BRIDGE.source,
type: BRIDGE.fetchType,
requestId,
url: requestUrl,
options: {
method: options.method || "GET",
transport: options.transport || "fetch",
referrer: options.referrer,
headers: {
accept: "application/json, text/plain, */*",
...(options.headers || {}),
},
body: options.body,
},
},
window.location.origin
);
});
if (!response.ok) {
const payloadMessage =
typeof response.payload === "string"
? cleanString(response.payload)
: firstNonEmptyString([
response.payload?.message,
response.payload?.error,
response.payload?.title,
response.payload?.detail,
]);
const debugHeaderKeys = Array.isArray(response.debug?.headerKeys)
? response.debug.headerKeys.join(", ")
: "";
const debugScopeKeys = Array.isArray(response.debug?.scopeKeys)
? response.debug.scopeKeys.join(", ")
: "";
const debugParts = [
debugHeaderKeys ? `captured headers: ${debugHeaderKeys}` : "",
debugScopeKeys ? `scopes: ${debugScopeKeys}` : "",
].filter(Boolean);
const debugSuffix = debugParts.length > 0
? ` | ${debugParts.join(" | ")}`
: "";
throw new Error(
`${payloadMessage || response.error || `Request failed (${response.status}) for ${requestUrl}`}${debugSuffix}`
);
}
return response.payload;
}
function shouldUseExtensionFetch(url) {
const normalized = resolveRequestUrl(url);
if (!normalized) {
return false;
}
try {
const parsedUrl = new URL(normalized, window.location.origin);
return parsedUrl.origin !== window.location.origin;
} catch {
return false;
}
}
function shouldUseSpaFrameFetch(url) {
if (!isHcmWelcomePage()) {
return false;
}
const normalized = resolveRequestUrl(url);
if (!normalized) {
return false;
}
try {
return new URL(normalized).origin === TARGET_ORIGIN;
} catch {
return false;
}
}
async function fetchJsonViaSpaFrame(url, options = {}) {
const requestUrl = resolveRequestUrl(url);
const bridgeHeaders = await requestBridgeHeaders({
url: requestUrl,
headers: options.headers,
body: options.body,
includeAllAuth: true,
}).catch(() => ({}));
await ensureSpaFrame();
const response = await new Promise((resolve, reject) => {
chrome.runtime.sendMessage(
{
type: "ARCH_PANEL_SPA_FRAME_FETCH",
url: requestUrl,
options: {
method: options.method || "GET",
referrer: options.referrer,
headers: buildExtensionFetchHeaders(
requestUrl,
options,
bridgeHeaders || {}
),
body: options.body,
},
},
(result) => {
const runtimeError = chrome.runtime.lastError;
if (runtimeError) {
reject(new Error(runtimeError.message));
return;
}
resolve(result || null);
}
);
});
if (!response || typeof response !== "object") {
throw new Error(`No response received for ${requestUrl}`);
}
if (
!response.ok &&
response.status === 0 &&
/SPA Workbench frame did not return a result/i.test(cleanString(response.error))
) {
throw new Error(response.error || "SPA Workbench iframe did not return a result.");
}
if (!response.ok) {
const payloadMessage =
typeof response.payload === "string"
? cleanString(response.payload)
: firstNonEmptyString([
response.payload?.message,
response.payload?.error,
response.payload?.title,
response.payload?.detail,
]);
const debugHeaderKeys = Array.isArray(response.debug?.headerKeys)
? response.debug.headerKeys.join(", ")
: "";
const debugFrameUrl = cleanString(response.debug?.frameUrl);
const debugUrl = cleanString(response.debug?.url);
const debugParts = [
debugUrl ? `url: ${debugUrl}` : "",
debugFrameUrl ? `frame: ${debugFrameUrl}` : "",
debugHeaderKeys ? `captured headers: ${debugHeaderKeys}` : "",
].filter(Boolean);
const debugSuffix = debugParts.length > 0
? ` | ${debugParts.join(" | ")}`
: "";
throw new Error(
`${
payloadMessage ||
response.error ||
`Request failed (${response.status}) for ${requestUrl}`
}${debugSuffix}`
);
}
return response.payload;
}
function ensureSpaFrame() {
const frameHost = ensureSpaFrameHost();
const existingFrame = document.getElementById(IDS.spaFrame);
const frameUrl = TARGET_APP_URL;
if (existingFrame instanceof HTMLIFrameElement) {
if (existingFrame.parentElement !== frameHost) {
frameHost.appendChild(existingFrame);
}
if (existingFrame.src !== frameUrl) {
navigateSpaFrame(existingFrame, frameUrl);
}
return waitForFrameLoad(existingFrame);
}
const frame = document.createElement("iframe");
frame.id = IDS.spaFrame;
frame.name = IDS.spaFrame;
frame.setAttribute("aria-hidden", "true");
frame.setAttribute("referrerpolicy", "no-referrer-when-downgrade");
frame.tabIndex = -1;
Object.assign(frame.style, {
position: "absolute",
width: "1440px",
height: "1000px",
opacity: "0",
pointerEvents: "none",
border: "0",
left: "0",
top: "0",
});
frameHost.appendChild(frame);
navigateSpaFrame(frame, frameUrl);
return waitForFrameLoad(frame);
}
function ensureSpaFrameHost() {
const existingHost = document.getElementById(IDS.spaFrameHost);
if (existingHost) {
return existingHost;
}
const host = document.createElement("div");
host.id = IDS.spaFrameHost;
host.setAttribute("aria-hidden", "true");
Object.assign(host.style, {
position: "fixed",
width: "1440px",
height: "1000px",
overflow: "hidden",
opacity: "0",
pointerEvents: "none",
border: "0",
left: "-20000px",
top: "0",
zIndex: "-1",
});
document.documentElement.appendChild(host);
return host;
}
function navigateSpaFrame(frame, frameUrl) {
frame.dataset.archPanelLoaded = "false";
frame.removeAttribute("src");
window.setTimeout(() => {
frame.src = frameUrl;
}, 0);
}
function waitForFrameLoad(frame) {
if (frame.dataset.archPanelLoaded === "true") {
return Promise.resolve();
}
return new Promise((resolve) => {
const timeout = window.setTimeout(() => {
resolve();
}, 12000);
frame.addEventListener(
"load",
() => {
frame.dataset.archPanelLoaded = "true";
window.clearTimeout(timeout);
window.setTimeout(resolve, 1200);
},
{ once: true }
);
});
}
async function fetchJsonViaExtension(url, options = {}) {
const requestUrl = resolveRequestUrl(url);
const bridgeHeaders = await requestBridgeHeaders({
url: requestUrl,
headers: options.headers,
body: options.body,
includeAllAuth: true,
});
const response = await new Promise((resolve, reject) => {
chrome.runtime.sendMessage(
{
type: "ARCH_PANEL_EXTENSION_FETCH",
url: requestUrl,
options: {
method: options.method || "GET",
referrer: options.referrer,
headers: buildExtensionFetchHeaders(
requestUrl,
options,
bridgeHeaders || {}
),
body: options.body,
},
},
(result) => {
const runtimeError = chrome.runtime.lastError;
if (runtimeError) {
reject(new Error(runtimeError.message));
return;
}
resolve(result || null);
}
);
});
if (!response || typeof response !== "object") {
throw new Error(`No response received for ${requestUrl}`);
}
if (!response.ok) {
const payloadMessage =
typeof response.payload === "string"
? cleanString(response.payload)
: firstNonEmptyString([
response.payload?.message,
response.payload?.error,
response.payload?.title,
response.payload?.detail,
]);
const debugHeaderKeys = Array.isArray(response.debug?.headerKeys)
? response.debug.headerKeys.join(", ")
: "";
const debugUrl = cleanString(response.debug?.url);
const debugParts = [
debugUrl ? `url: ${debugUrl}` : "",
debugHeaderKeys ? `captured headers: ${debugHeaderKeys}` : "",
].filter(Boolean);
const debugSuffix = debugParts.length > 0
? ` | ${debugParts.join(" | ")}`
: "";
throw new Error(
`${
payloadMessage ||
response.error ||
`Request failed (${response.status}) for ${requestUrl}`
}${debugSuffix}`
);
}
return response.payload;
}
function buildExtensionFetchHeaders(requestUrl, options = {}, bridgeHeaders = {}) {
const combinedHeaders = {
...(bridgeHeaders || {}),
...(appState.sessionAuthHeaders || {}),
accept: "application/json, text/plain, */*",
...(options.headers || {}),
};
return options.omitAuthHeaders ? stripAuthHeaders(combinedHeaders) : combinedHeaders;
}
function stripAuthHeaders(headers = {}) {
const sanitizedHeaders = {};
for (const [key, value] of Object.entries(headers || {})) {
const normalizedKey = cleanString(key).toLowerCase();
if (
normalizedKey === "authorization" ||
normalizedKey === "x-id-token" ||
normalizedKey === "spa-ts-authorization" ||
normalizedKey.endsWith("-authorization")
) {
continue;
}
sanitizedHeaders[key] = value;
}
return sanitizedHeaders;
}
async function requestBridgeHeaders(options = {}) {
await ensurePageBridge();
const requestId = `${Date.now()}-${++requestCounter}`;
return new Promise((resolve, reject) => {
const timeout = window.setTimeout(() => {
pendingBridgeRequests.delete(requestId);
reject(new Error("Timed out while requesting bridge headers"));
}, 30000);
pendingBridgeRequests.set(requestId, {
resolve,
reject,
timeout,
});
window.postMessage(
{
source: BRIDGE.source,
type: BRIDGE.headersType,
requestId,
options,
},
window.location.origin
);
});
}
function normalizeWorkload(item) {
const workloadId = cleanString(item?.workloadId || item?.id);
if (!workloadId) {
return null;
}
const opportunityForecastType = item?.opportunityForecastType || "";
const opportunityForecastTypeGroup = normalizeForecastType(
opportunityForecastType
);
return {
workloadId,
name: item?.name || "",
description: item?.description || "",
consumptionStartDate: item?.consumptionStartDate || "",
workloadStatus: item?.workloadStatus || "",
opportunityForecastType,
opportunityForecastTypeGroup,
opportunityId: cleanString(item?.opportunityId),
opportunityNumber: item?.opportunityNumber || "",
adjustedACR: toNumber(item?.adjustedACR),
rampMonths: toNumber(item?.rampMonths),
forecast: extractArray(item?.forecast, ["items", "content", "results", "data"]).map(
(forecastItem) => ({
month: forecastItem?.month || "",
year: toNumber(forecastItem?.year),
projectedConsumptionAmount: toNumber(
forecastItem?.projectedConsumptionAmount
),
adjustedConsumptionAmount: toNumber(
forecastItem?.adjustedConsumptionAmount
),
createdBy: forecastItem?.createdBy || "",
createdDate: forecastItem?.createdDate || "",
updatedBy: forecastItem?.updatedBy || "",
updatedDate: forecastItem?.updatedDate || "",
})
),
};
}
function isWonWorkload(workload) {
const forecastType = normalizeString(workload.opportunityForecastTypeGroup);
const workloadStatus = normalizeString(workload.workloadStatus);
return forecastType === "WON" || workloadStatus === "WON";
}
function normalizeForecastType(value) {
const text = String(value || "").trim();
if (!text) {
return "Unknown";
}
const [head] = text.split(" -");
return head.trim() || "Unknown";
}
function normalizeTeamValue(value) {
if (Array.isArray(value)) {
return value.map((item) => normalizeTeamValue(item)).join(" | ");
}
if (value && typeof value === "object") {
return Object.values(value).map((item) => normalizeTeamValue(item)).join(" | ");
}
return String(value || "");
}
function teamContainsUser(team, userEmail) {
return normalizeString(team).includes(normalizeString(userEmail));
}
function stripTransientFields(workload) {
return {
workloadId: workload.workloadId,
name: workload.name,
description: workload.description,
consumptionStartDate: workload.consumptionStartDate,
workloadStatus: workload.workloadStatus,
opportunityForecastType: workload.opportunityForecastType,
opportunityForecastTypeGroup: workload.opportunityForecastTypeGroup,
opportunityId: workload.opportunityId,
opportunityNumber: workload.opportunityNumber,
adjustedACR: workload.adjustedACR,
rampMonths: workload.rampMonths,
forecast: workload.forecast,
customerId: workload.customerId,
customerName: workload.customerName,
actions: workload.actions,
serviceRequests: workload.serviceRequests,
hasSR: workload.hasSR,
hasAction: workload.hasAction,
};
}
function createExportPayload(
user,
customers,
pendingServiceRequests = [],
resourceCurrentUser = null
) {
return {
generatedAt: new Date().toISOString(),
user,
resourceCurrentUser,
customers,
pendingServiceRequests,
};
}
function createDatasetSnapshot(exportPayload) {
const normalizedPayload = normalizeCachedExportPayload(exportPayload);
return {
user: normalizedPayload.user,
resourceCurrentUser: normalizedPayload.resourceCurrentUser,
customers: normalizedPayload.customers,
exportPayload: normalizedPayload,
summary: buildSummary(normalizedPayload),
};
}
function findWorkloadById(workloadId) {
if (!appState.dataset || !workloadId) {
return null;
}
for (const customer of appState.dataset.customers || []) {
for (const workload of customer.workloads || []) {
if (workload.workloadId === workloadId) {
return {
...workload,
customerId: workload.customerId || customer.customerId,
customerName: workload.customerName || customer.name,
};
}
}
}
return null;
}
function applyForecastUpdateToDataset(workloadId, payload) {
if (!appState.dataset || !workloadId) {
return;
}
const previousDetailModal = appState.detailModal
? {
scope: appState.detailModal.scope,
value: appState.detailModal.value,
sortKey: appState.detailModal.sortKey,
sortDirection: appState.detailModal.sortDirection,
type: appState.detailModal.type,
}
: null;
const updatesByPeriod = new Map(
(payload || []).map((item) => [
`${toNumber(item.year)}-${toNumber(item.month)}`,
toNumber(item.amount),
])
);
function updateForecast(workload) {
if (!workload || workload.workloadId !== workloadId) {
return workload;
}
workload.forecast = (workload.forecast || []).map((forecastItem) => {
const month = parseForecastMonthNumber(forecastItem.month);
const year = toNumber(forecastItem.year);
const key = `${year}-${month}`;
if (!updatesByPeriod.has(key)) {
return forecastItem;
}
return {
...forecastItem,
adjustedConsumptionAmount: updatesByPeriod.get(key),
updatedBy: appState.dataset.user?.userEmail || forecastItem.updatedBy,
updatedDate: new Date().toISOString(),
};
});
return workload;
}
for (const customer of appState.dataset.customers || []) {
customer.workloads = (customer.workloads || []).map((workload) =>
updateForecast(workload)
);
}
for (const customer of appState.dataset.exportPayload?.customers || []) {
customer.workloads = (customer.workloads || []).map((workload) =>
updateForecast(workload)
);
}
appState.dataset = createDatasetSnapshot(appState.dataset.exportPayload);
if (previousDetailModal?.scope && previousDetailModal.type !== "serviceRequests") {
restoreDetailModal(previousDetailModal);
}
void writeCachedDataset(appState.dataset.exportPayload);
}
function normalizeCachedExportPayload(exportPayload) {
const normalizedUser = {
userEmail: cleanString(exportPayload?.user?.userEmail),
name:
firstNonEmptyString([
exportPayload?.user?.name,
exportPayload?.user?.displayName,
exportPayload?.user?.fullName,
exportPayload?.user?.profile?.name,
exportPayload?.user?.userEmail,
]) || "",
avatar: cleanString(exportPayload?.user?.avatar || exportPayload?.user?.imageUrl),
administrator: Boolean(exportPayload?.user?.administrator),
};
const resourceCurrentUser = exportPayload?.resourceCurrentUser ?? null;
const normalizedCustomers = extractArray(exportPayload?.customers, ["customers"])
.map((customer) => ({
customerId: cleanString(customer?.customerId || customer?.id),
name: cleanString(customer?.name),
workloads: extractArray(customer?.workloads, ["workloads"])
.map((workload) => stripTransientFields(normalizeWorkloadForCache(workload)))
.filter((workload) => workload.workloadId),
}))
.filter((customer) => customer.customerId);
const pendingServiceRequests = normalizePendingServiceRequests(
extractArray(exportPayload?.pendingServiceRequests, [
"pendingServiceRequests",
"items",
"data",
"results",
])
);
return {
generatedAt:
firstNonEmptyString([exportPayload?.generatedAt]) || new Date().toISOString(),
user: normalizedUser,
resourceCurrentUser,
customers: normalizedCustomers,
pendingServiceRequests,
};
}
function normalizeWorkloadForCache(workload) {
const normalized = {
workloadId: cleanString(workload?.workloadId),
name: cleanString(workload?.name),
description: cleanString(workload?.description),
consumptionStartDate: workload?.consumptionStartDate || "",
workloadStatus: cleanString(workload?.workloadStatus),
opportunityForecastType: cleanString(workload?.opportunityForecastType),
opportunityForecastTypeGroup:
cleanString(workload?.opportunityForecastTypeGroup) ||
normalizeOpportunityForecastType(workload?.opportunityForecastType),
opportunityId: cleanString(workload?.opportunityId),
opportunityNumber: cleanString(workload?.opportunityNumber),
adjustedACR: toNumber(workload?.adjustedACR),
rampMonths: toNumber(workload?.rampMonths),
forecast: extractArray(workload?.forecast, ["forecast"]).map((entry) => ({
month: entry?.month || "",
year: entry?.year || "",
projectedConsumptionAmount: toNumber(entry?.projectedConsumptionAmount),
adjustedConsumptionAmount: toNumber(entry?.adjustedConsumptionAmount),
createdBy: entry?.createdBy || "",
createdDate: entry?.createdDate || "",
updatedBy: entry?.updatedBy || "",
updatedDate: entry?.updatedDate || "",
})),
customerId: cleanString(workload?.customerId),
customerName: cleanString(workload?.customerName),
actions: extractArray(workload?.actions, ["actions"]).map((action) => ({
owner: cleanString(action?.owner),
role: cleanString(action?.role),
name: cleanString(action?.name),
team: normalizeTeamValue(action?.team),
startDate: action?.startDate || "",
endDate: action?.endDate || "",
complexity: cleanString(action?.complexity),
})),
serviceRequests: extractArray(workload?.serviceRequests, [
"serviceRequests",
]).map((serviceRequest) => ({
srNumber: cleanString(serviceRequest?.srNumber),
status: cleanString(serviceRequest?.status),
team: normalizeTeamValue(serviceRequest?.team),
})),
hasSR: Boolean(workload?.hasSR),
hasAction: Boolean(workload?.hasAction),
};
return normalized;
}
function openCacheDatabase() {
return new Promise((resolve, reject) => {
if (!window.indexedDB) {
reject(new Error("IndexedDB is not available in this browser context."));
return;
}
const request = window.indexedDB.open(CACHE.dbName, CACHE.dbVersion);
request.onupgradeneeded = () => {
const database = request.result;
if (!database.objectStoreNames.contains(CACHE.storeName)) {
database.createObjectStore(CACHE.storeName, { keyPath: "key" });
}
};
request.onsuccess = () => {
resolve(request.result);
};
request.onerror = () => {
reject(request.error || new Error("Unable to open IndexedDB cache."));
};
});
}
async function withCacheStore(mode, callback) {
const database = await openCacheDatabase();
return new Promise((resolve, reject) => {
const transaction = database.transaction(CACHE.storeName, mode);
const store = transaction.objectStore(CACHE.storeName);
transaction.oncomplete = () => {
database.close();
};
transaction.onerror = () => {
const error =
transaction.error || new Error("IndexedDB transaction failed.");
database.close();
reject(error);
};
transaction.onabort = () => {
const error =
transaction.error || new Error("IndexedDB transaction was aborted.");
database.close();
reject(error);
};
callback(store, resolve, reject);
});
}
async function readCachedDataset() {
const record = await withCacheStore("readonly", (store, resolve, reject) => {
const request = store.get(CACHE.datasetKey);
request.onsuccess = () => {
const payload =
request.result?.exportPayload ||
request.result?.payload ||
request.result?.dataset?.exportPayload ||
request.result?.dataset ||
null;
resolve(payload);
};
request.onerror = () => {
reject(request.error || new Error("Unable to read the cached dataset."));
};
});
return record ? normalizeCachedExportPayload(record) : null;
}
async function writeCachedDataset(exportPayload) {
const normalizedPayload = normalizeCachedExportPayload(exportPayload);
await withCacheStore("readwrite", (store, resolve, reject) => {
const request = store.put({
key: CACHE.datasetKey,
updatedAt: new Date().toISOString(),
exportPayload: normalizedPayload,
});
request.onsuccess = () => {
resolve();
};
request.onerror = () => {
reject(request.error || new Error("Unable to write the cached dataset."));
};
});
}
async function readCachedTaskTypePayload() {
return withCacheStore("readonly", (store, resolve, reject) => {
const request = store.get(CACHE.taskTypeKey);
request.onsuccess = () => {
resolve(request.result?.payload || null);
};
request.onerror = () => {
reject(request.error || new Error("Unable to read the cached task types."));
};
});
}
async function writeCachedTaskTypePayload(payload) {
await withCacheStore("readwrite", (store, resolve, reject) => {
const request = store.put({
key: CACHE.taskTypeKey,
updatedAt: new Date().toISOString(),
payload,
});
request.onsuccess = () => {
resolve();
};
request.onerror = () => {
reject(request.error || new Error("Unable to write the cached task types."));
};
});
}
async function writeCachedWorklistToken(tokenPayload) {
const token = cleanString(tokenPayload?.token);
if (!token) {
return;
}
await withCacheStore("readwrite", (store, resolve, reject) => {
const request = store.put({
key: CACHE.worklistTokenKey,
updatedAt: new Date().toISOString(),
token,
sourceUrl: cleanString(tokenPayload?.url),
});
request.onsuccess = () => {
resolve();
};
request.onerror = () => {
reject(request.error || new Error("Unable to write the cached Worklist token."));
};
});
}
async function readCachedWorklistToken() {
const record = await withCacheStore("readonly", (store, resolve, reject) => {
const request = store.get(CACHE.worklistTokenKey);
request.onsuccess = () => {
resolve(request.result || null);
};
request.onerror = () => {
reject(request.error || new Error("Unable to read the cached Worklist token."));
};
});
return cleanString(record?.token);
}
function buildSummary(exportPayload) {
const customers = exportPayload.customers || [];
const allWorkloads = customers.flatMap((customer) => customer.workloads || []);
const eligibleWorkloads = allWorkloads
.filter((workload) =>
ALLOWED_STAGE_TYPES.includes(workload.opportunityForecastTypeGroup)
)
.sort((left, right) => right.adjustedACR - left.adjustedACR);
const totalEligibleAcr = eligibleWorkloads.reduce((sum, workload) => {
return sum + workload.adjustedACR;
}, 0);
const stageIndicators = ALLOWED_STAGE_TYPES.map((label) => {
const items = eligibleWorkloads.filter(
(workload) => workload.opportunityForecastTypeGroup === label
);
return {
label,
count: items.length,
totalAcr: items.reduce((sum, workload) => sum + workload.adjustedACR, 0),
};
});
const srStatus = {
trueCount: eligibleWorkloads.filter((workload) => workload.hasSR).length,
falseCount: eligibleWorkloads.filter((workload) => !workload.hasSR).length,
};
const actionStatus = {
trueCount: eligibleWorkloads.filter((workload) => workload.hasAction).length,
falseCount: eligibleWorkloads.filter((workload) => !workload.hasAction).length,
};
const pendingServiceRequests = exportPayload.pendingServiceRequests || [];
const srHours = {
betweenZeroAndFour: pendingServiceRequests.filter((serviceRequest) => {
const hours = toNumber(serviceRequest.totalHoursWorked);
return hours >= 0 && hours <= 4;
}),
aboveFour: pendingServiceRequests.filter((serviceRequest) => {
return toNumber(serviceRequest.totalHoursWorked) > 4;
}),
};
return {
generatedAtLabel: formatDateTime(exportPayload.generatedAt),
totalCustomers: customers.length,
totalWorkloads: allWorkloads.length,
allWorkloads,
eligibleWorkloads,
totalEligibleAcr,
stageIndicators,
srStatus,
actionStatus,
pendingServiceRequests,
srHours,
};
}
function buildWorkloadStartCalendar(
workloads,
year,
month,
view = "month",
anchorDateKey = ""
) {
const today = new Date();
const normalizedView = view === "week" ? "week" : "month";
const monthStart = new Date(year, month, 1);
const anchorDate = getCalendarAnchorDate(year, month, anchorDateKey);
const calendarStart =
normalizedView === "week"
? getWeekStartDate(anchorDate)
: getWeekStartDate(monthStart);
const dayCount = normalizedView === "week" ? 7 : 42;
const workloadMap = new Map();
for (const workload of workloads || []) {
const dateKey = getLocalDateKey(workload.consumptionStartDate);
if (!dateKey) {
continue;
}
const bucket = workloadMap.get(dateKey) || [];
bucket.push(workload);
workloadMap.set(dateKey, bucket);
}
const days = Array.from({ length: dayCount }, (_, index) => {
const date = new Date(calendarStart);
date.setDate(calendarStart.getDate() + index);
const dateKey = getLocalDateKey(date);
const workloadsForDay = workloadMap.get(dateKey) || [];
return {
dateKey,
dayNumber: date.getDate(),
isWeekStart: index % 7 === 0,
operationalWeekLabel: formatOperationalWeekLabel(
getOperationalWeekInfo(getCalendarRowWeekDate(date))
),
isCurrentMonth: normalizedView === "week" || date.getMonth() === month,
isToday: dateKey === getLocalDateKey(today),
workloads: workloadsForDay,
};
});
const weekEnd = new Date(calendarStart);
weekEnd.setDate(calendarStart.getDate() + 6);
const monthLabel = new Intl.DateTimeFormat("en-US", {
month: "long",
year: "numeric",
}).format(monthStart);
const weekLabel = `${formatDate(calendarStart)} - ${formatDate(weekEnd)}`;
return {
view: normalizedView,
periodLabel: normalizedView === "week" ? weekLabel : monthLabel,
days,
totalVisibleWorkloads: days.reduce(
(sum, day) => sum + day.workloads.length,
0
),
};
}
function getCalendarAnchorDate(year, month, anchorDateKey) {
const anchorDate = parseDatePreservingDateOnly(anchorDateKey);
if (!Number.isNaN(anchorDate.getTime())) {
return anchorDate;
}
return new Date(year, month, 1);
}
function getWeekStartDate(value) {
const date = parseDatePreservingDateOnly(value);
const weekStart = Number.isNaN(date.getTime()) ? new Date() : new Date(date);
weekStart.setDate(weekStart.getDate() - weekStart.getDay());
return weekStart;
}
function openDetailModal(scope, value) {
if (!appState.dataset) {
return;
}
const eligibleWorkloads = appState.dataset.summary.eligibleWorkloads;
let items = [];
let label = "";
let title = "";
if (scope === "stage") {
items = eligibleWorkloads.filter(
(workload) => workload.opportunityForecastTypeGroup === value
);
label = `Opportunity type: ${value}`;
title = `${value} workloads`;
} else if (scope === "sr") {
const flag = value === "true";
items = eligibleWorkloads.filter((workload) => workload.hasSR === flag);
label = "Status de SRs";
title = flag ? "Workloads com SR" : "Workloads sem SR";
} else if (scope === "action") {
const flag = value === "true";
items = eligibleWorkloads.filter((workload) => workload.hasAction === flag);
label = "Status de Consumption Plan/action";
title = flag ? "Workloads com action" : "Workloads sem action";
} else if (scope === "sr-hours") {
const pendingServiceRequests = appState.dataset.summary.pendingServiceRequests || [];
const isAboveFour = value === "above-4";
items = pendingServiceRequests.filter((serviceRequest) => {
const hours = toNumber(serviceRequest.totalHoursWorked);
return isAboveFour ? hours > 4 : hours >= 0 && hours <= 4;
});
items.sort((left, right) => {
return toNumber(left.totalHoursWorked) - toNumber(right.totalHoursWorked);
});
label = "SRs por horas reportadas";
title = isAboveFour ? "SRs acima de 4h" : "SRs entre 0 e 4h";
} else if (scope === "calendar-day") {
const allWorkloads = appState.dataset.summary.allWorkloads || [];
items = allWorkloads.filter((workload) => {
return getLocalDateKey(workload.consumptionStartDate) === value;
});
label = "Workload start calendar";
title = `Workloads starting ${formatDate(value) || value}`;
}
appState.detailModal = {
label,
title,
items,
totalAcr:
scope === "sr-hours"
? 0
: items.reduce((sum, workload) => sum + workload.adjustedACR, 0),
sortKey: DETAIL_SORT_KEYS.adjustedACR,
sortDirection: "desc",
scope,
value,
type: scope === "sr-hours" ? "serviceRequests" : "workloads",
};
renderModal();
}
function openActionFormModal(workloadId) {
appState.actionFormModal = {
workloadId,
errorMessage: "",
isSubmitting: false,
};
renderModal();
}
function openRampComparisonModal(workloadId) {
const workload = findWorkloadById(workloadId);
if (!workload) {
return;
}
appState.rampComparisonModal = {
workload,
rows: buildRampForecastComparisonRows([workload]),
};
renderModal();
}
function openForecastUpdateConfirmModal() {
if (!appState.rampComparisonModal || !appState.dataset) {
return;
}
const payload = createForecastUpdatePayload(
appState.rampComparisonModal.rows || []
);
appState.forecastUpdateConfirmModal = {
workloadId: appState.rampComparisonModal.workload.workloadId,
payload,
isSubmitting: false,
errorMessage: "",
};
renderModal();
}
async function submitForecastUpdate() {
const modal = appState.forecastUpdateConfirmModal;
if (!modal || modal.isSubmitting) {
return;
}
appState.forecastUpdateConfirmModal = {
...modal,
isSubmitting: true,
errorMessage: "",
};
renderModal();
try {
const workloadId = cleanString(modal.workloadId);
const url = `${API_ENDPOINTS.workloadServiceRequests}/${encodeURIComponent(
workloadId
)}/forecast`;
const headers = {
accept: "*/*",
"content-type": "application/json",
};
await fetchJson(url, {
method: "PATCH",
omitAuthHeaders: true,
headers,
body: JSON.stringify(modal.payload),
});
applyForecastUpdateToDataset(workloadId, modal.payload);
const updatedWorkload = findWorkloadById(workloadId);
appState.forecastUpdateConfirmModal = null;
if (updatedWorkload) {
appState.rampComparisonModal = {
workload: updatedWorkload,
rows: buildRampForecastComparisonRows([updatedWorkload]),
};
}
renderModal();
} catch (error) {
appState.forecastUpdateConfirmModal = {
...modal,
isSubmitting: false,
errorMessage: getErrorMessage(error),
};
renderModal();
}
}
function createForecastUpdatePayload(rows) {
const userEmail = appState.dataset?.user?.userEmail || "";
return (rows || [])
.filter((row) => row.year && row.month)
.map((row) => ({
year: toNumber(row.year),
month: toNumber(row.month),
amount: toNumber(row.calculatedValue),
updatedBy: userEmail,
}));
}
async function openTimeEntriesDrawer(srNumber) {
const normalizedSrNumber = cleanString(srNumber);
if (!normalizedSrNumber) {
return;
}
appState.timeEntriesDrawer = {
srNumber: normalizedSrNumber,
status: "loading",
errorMessage: "",
payload: null,
items: [],
};
renderModal();
try {
const response = await fetchTimeEntriesSummary(normalizedSrNumber);
appState.timeEntriesDrawer = {
srNumber: normalizedSrNumber,
status: "ready",
errorMessage: "",
payload: response.payload,
items: normalizeTimeEntriesSummary(response.payload),
};
} catch (error) {
appState.timeEntriesDrawer = {
srNumber: normalizedSrNumber,
status: "error",
errorMessage: getErrorMessage(error),
payload: null,
items: [],
};
}
renderModal();
}
async function fetchTimeEntriesSummary(srNumber) {
const url = `${COMCIP_REQUEST.timeEntriesSummaryUrl}?srNumber=${encodeURIComponent(
srNumber
)}`;
const response = await sendComcipGetMessage(url);
if (!response?.ok) {
const payloadMessage =
typeof response?.payload === "string"
? cleanString(response.payload)
: firstNonEmptyString([
response?.payload?.message,
response?.payload?.error,
response?.payload?.title,
response?.payload?.detail,
]);
throw new Error(
payloadMessage ||
response?.error ||
`COMCIP request failed (${response?.status || 0}).`
);
}
return response;
}
async function openTimeManagementModal() {
const selectedDate = getDateInputValue(new Date());
appState.timeManagementModal = createTimeManagementLoadingState(selectedDate);
renderModal();
await loadTimeManagementData(selectedDate);
}
async function loadTimeManagementData(selectedDate) {
const normalizedDate = cleanString(selectedDate) || getDateInputValue(new Date());
const requestId = `${Date.now()}-${++requestCounter}`;
appState.timeManagementDatePicker = {
...appState.timeManagementDatePicker,
isOpen: false,
};
appState.timeManagementModal = {
...(appState.timeManagementModal || {}),
requestId,
selectedDate: normalizedDate,
status: "loading",
errorMessage: "",
};
renderModal();
try {
const resourcePartyId = getCurrentUserResourcePartyId();
if (!resourcePartyId) {
throw new Error("resourceCurrentUser.resourcePartyId não está disponível.");
}
const weekInfoPayload = await fetchInfoWeekRequest(normalizedDate);
const weekDays = normalizeInfoWeekResponse(weekInfoPayload);
const weekId = cleanString(weekDays[0]?.weekId);
if (!weekId) {
throw new Error("WeekId não encontrado no retorno de infoWeekRequest.");
}
const timeEntryWeekUrl = buildTimeEntryWeekUrl(resourcePartyId, weekId);
const [srPayload, taskTypePayload, timeEntryWeekPayload] = await Promise.all([
fetchActiveServiceRequests(resourcePartyId, weekId),
fetchTaskTypeRequest(),
fetchTimeEntryWeek(timeEntryWeekUrl),
]);
const timeEntryRows = normalizeTimeManagementEntryRows(
timeEntryWeekPayload,
weekDays
);
const serviceRequests = mergeTimeManagementServiceRequests(
normalizeTimeManagementServiceRequests(srPayload),
timeEntryRows
);
const activitySrNumbers = getActivityRequestSrNumbers(srPayload, serviceRequests);
let activityRequestUrl = activitySrNumbers.length
? buildActivityRequestUrl(activitySrNumbers)
: "";
let activityPayload = activityRequestUrl
? await fetchComcipPayload(activityRequestUrl, "activityRequest")
: null;
if (activityRequestUrl && !hasActivityRequestItems(activityPayload)) {
activityRequestUrl = buildActivityRequestUrl(
ACTIVITY_REQUEST_REFERENCE_SR_NUMBERS
);
activityPayload = await fetchComcipPayload(
activityRequestUrl,
"activityRequest"
);
}
const activityOptionsBySr = normalizeActivityRequest(
activityPayload,
serviceRequests
);
const taskTypes = normalizeTaskTypeRequest(taskTypePayload);
const rows = timeEntryRows.length
? timeEntryRows
: [createBlankTimeManagementEntryRow()];
const selectedSrNumber = rows[0]?.selectedSrNumber || "";
const normalizedRows = rows.map((row, index) =>
normalizeTimeManagementEntryRowSelection(
row,
index,
activityOptionsBySr,
taskTypes
)
);
if (appState.timeManagementModal?.requestId !== requestId) {
return;
}
appState.timeManagementModal = {
requestId,
selectedDate: normalizedDate,
status: "ready",
errorMessage: "",
weekId,
weekDays,
serviceRequests,
selectedSrNumber,
activityOptionsBySr,
activityRequestUrl,
activityRequestPayload: activityPayload,
timeEntryRows: normalizedRows,
selectedActivityValue:
normalizedRows[0]?.selectedActivityValue || "",
taskTypes,
selectedTaskTypeValue: normalizedRows[0]?.selectedTaskTypeValue || "",
};
renderModal();
} catch (error) {
if (appState.timeManagementModal?.requestId !== requestId) {
return;
}
appState.timeManagementModal = {
...(appState.timeManagementModal || {}),
requestId,
selectedDate: normalizedDate,
status: "error",
errorMessage: getErrorMessage(error),
};
renderModal();
}
}
function createTimeManagementLoadingState(selectedDate) {
return {
requestId: "",
selectedDate,
status: "loading",
errorMessage: "",
weekId: "",
weekDays: [],
serviceRequests: [],
selectedSrNumber: "",
activityOptionsBySr: {},
activityRequestUrl: "",
activityRequestPayload: null,
selectedActivityValue: "",
taskTypes: [],
selectedTaskTypeValue: "",
timeEntryRows: [],
};
}
function getTimeManagementRows(modal) {
return modal.timeEntryRows?.length
? modal.timeEntryRows
: [createBlankTimeManagementEntryRow()];
}
function createBlankTimeManagementEntryRow(srNumber = "") {
return {
id: `row-${Date.now()}-${Math.random().toString(16).slice(2)}`,
selectedSrNumber: cleanString(srNumber),
selectedActivityValue: "",
selectedTaskTypeValue: "",
dayHours: {},
source: "blank",
};
}
function addTimeManagementEntryRow(type) {
if (!appState.timeManagementModal) {
return;
}
const srNumber = type === "non-service" ? "non-service-sr" : "";
const nextRows = [
...getTimeManagementRows(appState.timeManagementModal),
createBlankTimeManagementEntryRow(srNumber),
];
appState.timeManagementModal = {
...appState.timeManagementModal,
timeEntryRows: nextRows,
};
renderModal();
}
function removeTimeManagementEntryRow(rowId) {
if (!appState.timeManagementModal) {
return;
}
const normalizedRowId = cleanString(rowId);
const remainingRows = getTimeManagementRows(appState.timeManagementModal).filter(
(row) => cleanString(row?.id) !== normalizedRowId
);
const nextRows = remainingRows.length
? remainingRows
: [createBlankTimeManagementEntryRow()];
appState.timeManagementModal = {
...appState.timeManagementModal,
timeEntryRows: nextRows,
selectedSrNumber: nextRows[0]?.selectedSrNumber || "",
selectedActivityValue: nextRows[0]?.selectedActivityValue || "",
selectedTaskTypeValue: nextRows[0]?.selectedTaskTypeValue || "",
};
renderModal();
}
function getTimeManagementRowById(modal, rowId) {
const rows = getTimeManagementRows(modal);
const normalizedRowId = cleanString(rowId);
return (
rows.find((row) => cleanString(row?.id) === normalizedRowId) ||
rows[0] ||
null
);
}
function updateTimeManagementRows(modal, rowId, nextRow) {
const rows = getTimeManagementRows(modal);
const normalizedRowId = cleanString(rowId || nextRow?.id);
return rows.map((row, index) => {
const isTarget = normalizedRowId
? cleanString(row?.id) === normalizedRowId
: index === 0;
return isTarget
? {
...row,
...nextRow,
id: row?.id || nextRow?.id || `row-${index + 1}`,
}
: row;
});
}
function updateTimeManagementSelectedSr(srNumber, rowId = "") {
if (!appState.timeManagementModal) {
return;
}
const targetRow = getTimeManagementRowById(appState.timeManagementModal, rowId);
const selectedServiceRequest = findTimeManagementServiceRequest(
appState.timeManagementModal,
srNumber
);
const selectedSrNumber = selectedServiceRequest?.srNumber || cleanString(srNumber);
const nextRow = {
...(targetRow || createBlankTimeManagementEntryRow()),
selectedSrNumber,
selectedActivityValue: "",
selectedTaskTypeValue: "",
};
const nextRows = updateTimeManagementRows(
appState.timeManagementModal,
rowId,
nextRow
);
appState.timeManagementModal = {
...appState.timeManagementModal,
timeEntryRows: nextRows,
selectedSrNumber: nextRows[0]?.selectedSrNumber || selectedSrNumber,
selectedActivityValue: nextRows[0]?.selectedActivityValue || "",
selectedTaskTypeValue: nextRows[0]?.selectedTaskTypeValue || "",
};
renderModal();
}
function updateTimeManagementField(fieldName, inputValue, rowId = "") {
if (!appState.timeManagementModal) {
return;
}
const targetRow = getTimeManagementRowById(appState.timeManagementModal, rowId);
const normalizedInput = cleanString(inputValue);
let value = normalizedInput;
if (fieldName === "selectedActivityValue") {
const options = getTimeManagementActivityOptions(
appState.timeManagementModal,
targetRow?.selectedSrNumber || appState.timeManagementModal.selectedSrNumber
);
value = findOptionValue(options, normalizedInput) || normalizedInput;
}
if (fieldName === "selectedTaskTypeValue") {
const options = getTimeManagementTaskTypeOptions(
appState.timeManagementModal,
targetRow?.selectedSrNumber || appState.timeManagementModal.selectedSrNumber
);
value =
findOptionValue(options, normalizedInput) || normalizedInput;
}
const nextRows = updateTimeManagementRows(appState.timeManagementModal, rowId, {
...(targetRow || createBlankTimeManagementEntryRow()),
[fieldName]: value,
});
appState.timeManagementModal = {
...appState.timeManagementModal,
timeEntryRows: nextRows,
[fieldName]: value,
};
}
function updateTimeManagementDayHours(rowId, dateKey, inputValue) {
if (!appState.timeManagementModal) {
return;
}
const targetRow = getTimeManagementRowById(appState.timeManagementModal, rowId);
const nextRow = {
...(targetRow || createBlankTimeManagementEntryRow()),
dayHours: {
...(targetRow?.dayHours || {}),
[cleanString(dateKey)]: cleanString(inputValue),
},
};
appState.timeManagementModal = {
...appState.timeManagementModal,
timeEntryRows: updateTimeManagementRows(
appState.timeManagementModal,
rowId,
nextRow
),
};
}
function selectTimeManagementComboboxOption(field, value, rowId = "") {
if (field === "sr") {
updateTimeManagementSelectedSr(value || "", rowId);
return;
}
if (field === "activity") {
updateTimeManagementField("selectedActivityValue", value || "", rowId);
renderModal();
return;
}
if (field === "taskType") {
updateTimeManagementField("selectedTaskTypeValue", value || "", rowId);
renderModal();
}
}
function filterTimeManagementCombobox(input) {
const query = normalizeString(input.value);
const combobox = input.closest(".arch-panel-extension-combobox");
if (!combobox) {
return;
}
const options = Array.from(
combobox.querySelectorAll(".arch-panel-extension-combobox-option")
);
for (const option of options) {
const searchText = normalizeString(option.getAttribute("data-search") || "");
option.hidden = Boolean(query) && !searchText.includes(query);
}
setActiveTimeManagementComboboxOption(
combobox,
getVisibleTimeManagementComboboxOptions(combobox).find((option) =>
option.classList.contains("is-selected")
) || getVisibleTimeManagementComboboxOptions(combobox)[0]
);
}
function handleTimeManagementComboboxKeydown(event, input) {
const combobox = input.closest(".arch-panel-extension-combobox");
if (!combobox) {
return;
}
if (event.key === "Escape") {
clearActiveTimeManagementComboboxOption(combobox);
input.blur();
return;
}
const visibleOptions = getVisibleTimeManagementComboboxOptions(combobox);
if (!visibleOptions.length) {
return;
}
event.preventDefault();
if (event.key === "Enter") {
const activeOption =
visibleOptions.find((option) => option.classList.contains("is-active")) ||
visibleOptions[0];
activeOption.click();
return;
}
const activeIndex = visibleOptions.findIndex((option) =>
option.classList.contains("is-active")
);
const selectedIndex = visibleOptions.findIndex((option) =>
option.classList.contains("is-selected")
);
const baseIndex = activeIndex >= 0 ? activeIndex : selectedIndex;
let nextIndex = 0;
if (event.key === "ArrowDown") {
nextIndex = baseIndex >= 0 ? baseIndex + 1 : 0;
} else if (event.key === "ArrowUp") {
nextIndex = baseIndex >= 0 ? baseIndex - 1 : visibleOptions.length - 1;
} else if (event.key === "Home") {
nextIndex = 0;
} else if (event.key === "End") {
nextIndex = visibleOptions.length - 1;
}
if (nextIndex < 0) {
nextIndex = visibleOptions.length - 1;
} else if (nextIndex >= visibleOptions.length) {
nextIndex = 0;
}
setActiveTimeManagementComboboxOption(combobox, visibleOptions[nextIndex]);
}
function getVisibleTimeManagementComboboxOptions(combobox) {
return Array.from(
combobox.querySelectorAll(".arch-panel-extension-combobox-option")
).filter((option) => !option.hidden);
}
function setActiveTimeManagementComboboxOption(combobox, option) {
clearActiveTimeManagementComboboxOption(combobox);
if (!option) {
return;
}
option.classList.add("is-active");
combobox
.querySelector("input[data-combobox-field]")
?.setAttribute("aria-activedescendant", option.id || "");
option.scrollIntoView({ block: "nearest" });
}
function clearActiveTimeManagementComboboxOption(combobox) {
combobox
.querySelectorAll(".arch-panel-extension-combobox-option.is-active")
.forEach((option) => option.classList.remove("is-active"));
combobox
.querySelector("input[data-combobox-field]")
?.removeAttribute("aria-activedescendant");
}
async function fetchInfoWeekRequest(selectedDate) {
const dateValue = formatDateForTimeEntryRequest(selectedDate);
const url = `${COMCIP_REQUEST.timeEntryWeekDetailsUrl}?dateValue=${encodeURIComponent(
dateValue
)}`;
return fetchComcipPayload(url, "infoWeekRequest");
}
async function fetchActiveServiceRequests(resourceId, weekId) {
const url = `${COMCIP_REQUEST.activeServiceRequestsUrl}?resourceId=${encodeURIComponent(
resourceId
)}&weekId=${encodeURIComponent(weekId)}`;
return fetchComcipPayload(url, "sRRequest");
}
function buildTimeEntryWeekUrl(resourceId, weekId) {
return `${COMCIP_REQUEST.timeEntriesUrl}?resourceId=${encodeURIComponent(
resourceId
)}&sortBy=createdDate&sortOrder=ASC&weekId=${encodeURIComponent(weekId)}`;
}
async function fetchTimeEntryWeek(url) {
return fetchComcipPayload(url, "timeEntryWeek");
}
async function fetchActivityRequest(srNumbers) {
return fetchComcipPayload(buildActivityRequestUrl(srNumbers), "activityRequest");
}
function buildActivityRequestUrl(srNumbers) {
const query = buildServiceRequestActivityQuery(srNumbers);
const fields =
"SrNumber,SRActivities_c,ProblemDescription,AccountPartyUniqueName,ServiceNew_c,OptyOwner_c,OptyName_c,OptyNumber_c,ReportedByPartyUniqueName,StatusCdMeaning,CountryText_c,AssigneePersonName,OptyStatus_c";
return [
COMCIP_REQUEST.activityServiceRequestsUrl,
"?expand=ServiceRequest_SRToInternalSR_Tgt",
`&fields=${encodeActivityRequestParam(fields)}`,
"&limit=200",
"&onlyData=true",
`&q=${encodeActivityRequestParam(query)}`,
].join("");
}
function encodeActivityRequestParam(value) {
return encodeURIComponent(String(value || ""))
.replace(/'/g, "%27")
.replace(/\(/g, "%28")
.replace(/\)/g, "%29");
}
async function fetchTaskTypeRequest() {
if (appState.taskTypeCache.promise) {
return appState.taskTypeCache.promise;
}
appState.taskTypeCache.promise = (async () => {
return readCachedTaskTypePayload().catch(() => null);
})().finally(() => {
appState.taskTypeCache.promise = null;
});
return appState.taskTypeCache.promise;
}
async function refreshTaskTypeCache() {
const payload = await fetchComcipPayload(
COMCIP_REQUEST.taskTypeUrl,
"taskTypeRequest"
);
await writeCachedTaskTypePayload(payload);
return payload;
}
async function fetchComcipPayload(url, requestName) {
const response = await sendComcipGetMessage(url);
if (!response?.ok) {
const payloadMessage =
typeof response?.payload === "string"
? cleanString(response.payload)
: firstNonEmptyString([
response?.payload?.message,
response?.payload?.error,
response?.payload?.title,
response?.payload?.detail,
]);
throw new Error(
`${requestName}: ${
payloadMessage ||
response?.error ||
`COMCIP request failed (${response?.status || 0}).`
}`
);
}
return response.payload;
}
function hasActivityRequestItems(payload) {
const count = Number(payload?.count);
if (Number.isFinite(count) && count > 0) {
return true;
}
return extractArray(payload, ["items"]).length > 0;
}
function getCurrentUserResourcePartyId() {
const resourceCurrentUser = appState.dataset?.resourceCurrentUser;
const directResourcePartyId = cleanString(
getFieldValue(resourceCurrentUser, [
"resourcePartyId",
"ResourcePartyId",
"ResourcePartyID",
"partyId",
"PartyId",
])
);
if (directResourcePartyId) {
return directResourcePartyId;
}
const candidates = Array.isArray(resourceCurrentUser)
? resourceCurrentUser
: extractArray(resourceCurrentUser, ["items", "data", "results", "content"]);
const source = candidates[0] || resourceCurrentUser;
return cleanString(
getFieldValue(source, [
"resourcePartyId",
"ResourcePartyId",
"ResourcePartyID",
"partyId",
"PartyId",
])
);
}
function normalizeInfoWeekResponse(payload) {
return extractArray(payload, ["items", "data", "results", "content"])
.map((item) => {
const rawDate = cleanString(getFieldValue(item, ["Date", "date"]));
const parsedDate = parseTimeEntryDate(rawDate);
return {
date: rawDate,
weekId: cleanString(getFieldValue(item, ["WeekId", "weekId"])),
dayLabel: Number.isNaN(parsedDate.getTime())
? rawDate
: new Intl.DateTimeFormat("en-US", { weekday: "short" }).format(parsedDate),
monthLabel: Number.isNaN(parsedDate.getTime())
? rawDate
: new Intl.DateTimeFormat("en-US", {
month: "short",
day: "numeric",
}).format(parsedDate),
};
})
.filter((item) => item.date || item.weekId);
}
function normalizeTimeManagementServiceRequests(payload) {
return extractArray(payload, ["items", "data", "results", "content"])
.map((item) => {
const srNumber = cleanString(
getFieldValue(item, [
"srNumber",
"SrNumber",
"SRNumber",
"serviceRequestNumber",
"ServiceRequestNumber",
"serviceRequest",
"ServiceRequest",
])
);
const srTitle = cleanString(
getFieldValue(item, ["srTitle", "SrTitle", "SRTitle", "title", "Title"])
);
const fallbackTitle = cleanString(
getFieldValue(item, ["problemDescription", "ProblemDescription"])
);
return {
srNumber,
srTitle,
label: formatTimeManagementServiceRequestLabel({
srNumber,
srTitle: srTitle || fallbackTitle,
}),
};
})
.filter((item) => item.srNumber);
}
function formatTimeManagementServiceRequestLabel(serviceRequest) {
const srNumber = cleanString(serviceRequest?.srNumber);
const srTitle = cleanString(serviceRequest?.srTitle || serviceRequest?.label);
if (srTitle && srNumber) {
return `${srTitle} (${srNumber})`;
}
return srTitle || srNumber;
}
function mergeTimeManagementServiceRequests(serviceRequests, timeEntryRows) {
const merged = [...(serviceRequests || [])];
const seen = new Set(merged.map((item) => normalizeString(item.srNumber)));
for (const row of timeEntryRows || []) {
const srNumber = cleanString(row?.selectedSrNumber);
const key = normalizeString(srNumber);
if (!srNumber || seen.has(key)) {
continue;
}
merged.push({
srNumber,
srTitle: srNumber === "non-service-sr" ? "Non-service request" : srNumber,
label: formatTimeManagementServiceRequestLabel({
srNumber,
srTitle: srNumber === "non-service-sr" ? "Non-service request" : srNumber,
}),
});
seen.add(key);
}
return merged;
}
function normalizeTimeManagementEntryRows(payload, weekDays) {
return extractArray(payload, ["items", "data", "results", "content"]).map(
(item, index) => {
const srNumber = getTimeEntrySrNumber(item) || "non-service-sr";
const activityValue = getTimeEntryActivity(item);
const taskTypeValue = getTimeEntryTaskType(item);
return {
id: `entry-${index + 1}`,
selectedSrNumber: srNumber,
selectedActivityValue: activityValue,
selectedTaskTypeValue: taskTypeValue,
dayHours: getTimeEntryWeekDayHours(item, weekDays),
source: "timeEntryWeek",
};
}
);
}
function getTimeEntryWeekDayHours(item, weekDays = []) {
const dayHours = {};
for (const day of weekDays || []) {
const dateKey = getTimeManagementDayKey(day);
const weekdayName = getTimeManagementWeekdayName(day);
const value = getTimeEntryWeekdayTaskHours(item, weekdayName);
if (dateKey) {
dayHours[dateKey] = formatTimeManagementHourValue(value);
}
}
return dayHours;
}
function getTimeManagementWeekdayName(day) {
const parsedDate = parseTimeEntryDate(day?.date);
if (!Number.isNaN(parsedDate.getTime())) {
return new Intl.DateTimeFormat("en-US", { weekday: "long" })
.format(parsedDate)
.toLowerCase();
}
const label = cleanString(day?.dayLabel || day?.date).toLowerCase();
const aliases = {
sun: "sunday",
sunday: "sunday",
mon: "monday",
monday: "monday",
tue: "tuesday",
tuesday: "tuesday",
wed: "wednesday",
wednesday: "wednesday",
thu: "thursday",
thursday: "thursday",
fri: "friday",
friday: "friday",
sat: "saturday",
saturday: "saturday",
};
return aliases[label.slice(0, 3)] || aliases[label] || "";
}
function getTimeEntryWeekdayTaskHours(item, weekdayName) {
if (!weekdayName) {
return "";
}
const titleCaseWeekday =
weekdayName.charAt(0).toUpperCase() + weekdayName.slice(1);
const compactKey = `${weekdayName}TaskHours`;
const titleKey = `${titleCaseWeekday}TaskHours`;
const snakeKey = `${weekdayName}_task_hours`;
return getTimeEntryField(item, [
compactKey,
titleKey,
snakeKey,
`${weekdayName}.taskHours`,
`${titleCaseWeekday}.TaskHours`,
]);
}
function formatTimeManagementHourValue(value) {
if (value === undefined || value === null || value === "") {
return "";
}
return cleanString(value);
}
function normalizeTimeManagementEntryRowSelection(
row,
index,
activityOptionsBySr,
taskTypes
) {
const srNumber = cleanString(row?.selectedSrNumber);
const activitySrKey = Object.keys(activityOptionsBySr || {}).find(
(key) => normalizeString(key) === normalizeString(srNumber)
);
const activityOptions =
activityOptionsBySr?.[srNumber] || activityOptionsBySr?.[activitySrKey] || [];
const taskTypeOptions = filterTaskTypesForServiceRequest(taskTypes || [], srNumber);
return {
...row,
id: cleanString(row?.id) || `entry-${index + 1}`,
selectedSrNumber: srNumber,
selectedActivityValue:
findOptionValue(activityOptions, row?.selectedActivityValue) ||
cleanString(row?.selectedActivityValue) ||
"",
selectedTaskTypeValue:
findOptionValue(taskTypeOptions, row?.selectedTaskTypeValue) ||
cleanString(row?.selectedTaskTypeValue) ||
"",
dayHours: row?.dayHours || {},
};
}
function getActivityRequestSrNumbers(srPayload, serviceRequests) {
const normalizedSrNumbers = [
...(serviceRequests || []).map((item) => item.srNumber),
...collectSrNumbers(srPayload),
];
const seen = new Set();
return normalizedSrNumbers
.map((srNumber) => cleanString(srNumber).toUpperCase())
.filter((srNumber) => /^SR\d+$/i.test(srNumber))
.filter((srNumber) => {
const key = normalizeString(srNumber);
if (seen.has(key)) {
return false;
}
seen.add(key);
return true;
});
}
function collectSrNumbers(value, seen = new WeakSet()) {
if (!value) {
return [];
}
if (typeof value === "string" || typeof value === "number") {
return Array.from(String(value).matchAll(/\bSR\d+\b/gi), (match) => match[0]);
}
if (typeof value !== "object") {
return [];
}
if (seen.has(value)) {
return [];
}
seen.add(value);
if (Array.isArray(value)) {
return value.flatMap((item) => collectSrNumbers(item, seen));
}
return Object.values(value).flatMap((nestedValue) =>
collectSrNumbers(nestedValue, seen)
);
}
function normalizeActivityRequest(payload, serviceRequests) {
const activityOptionsBySr = {};
const activityItemsBySr = new Map();
for (const item of extractArray(payload, ["items", "data", "results", "content"])) {
const srNumber = cleanString(getFieldValue(item, ["SrNumber", "srNumber"]));
const srKey = normalizeString(srNumber);
if (srKey && !activityItemsBySr.has(srKey)) {
activityItemsBySr.set(srKey, item);
}
}
for (const serviceRequest of serviceRequests || []) {
const srNumber = cleanString(serviceRequest?.srNumber);
const srKey = normalizeString(srNumber);
const activityItem = activityItemsBySr.get(srKey);
const options = activityItem ? normalizeSrActivityFieldOptions(activityItem) : [];
if (srNumber) {
activityOptionsBySr[srNumber] = options;
}
}
return activityOptionsBySr;
}
function normalizeSrActivityFieldOptions(item) {
const activityText = firstNonEmptyString([
getFieldValue(item, ["SRActivities_c", "srActivities_c"]),
getFieldValue(item, [
"ServiceRequest_SRToInternalSR_Tgt.SRActivities_c",
"ServiceRequest_SRToInternalSR_Tgt.srActivities_c",
]),
]);
if (activityText) {
return parseSemicolonActivityOptions(activityText);
}
return normalizeActivityOptions(item);
}
function normalizeActivityOptions(value) {
const recursiveActivityTextValues = collectActivityTextValues(value);
if (recursiveActivityTextValues.length) {
return parseSemicolonActivityOptions(recursiveActivityTextValues.join(";"));
}
const directActivityValue =
value && typeof value === "object" && !Array.isArray(value)
? getFieldValue(value, ["SRActivities_c", "srActivities_c", "activities"])
: "";
const nestedActivityValue =
value && typeof value === "object" && !Array.isArray(value)
? getFieldValue(value, [
"ServiceRequest_SRToInternalSR_Tgt.SRActivities_c",
"ServiceRequest_SRToInternalSR_Tgt.srActivities_c",
])
: "";
const nestedActivityObject =
value && typeof value === "object" && !Array.isArray(value)
? getFieldValue(value, ["ServiceRequest_SRToInternalSR_Tgt"])
: "";
const explicitActivityText = firstNonEmptyString([
directActivityValue && typeof directActivityValue !== "object"
? directActivityValue
: "",
nestedActivityValue && typeof nestedActivityValue !== "object"
? nestedActivityValue
: "",
]);
if (explicitActivityText) {
return parseSemicolonActivityOptions(explicitActivityText);
}
const activityValue =
directActivityValue ||
nestedActivityValue ||
nestedActivityObject ||
value;
const rawItems = Array.isArray(value)
? value
: activityValue && typeof activityValue === "object"
? extractArray(activityValue, [
"items",
"data",
"results",
"content",
"activities",
"ServiceRequest_SRToInternalSR_Tgt",
])
: [];
const textValue =
activityValue && typeof activityValue === "object"
? ""
: cleanString(activityValue);
const parsedTextItems = parseActivityTextOptions(textValue);
const items = rawItems.length
? rawItems
: parsedTextItems;
const seen = new Set();
return items
.map((item) => {
const optionValue =
item && typeof item === "object"
? firstNonEmptyString([
item.activityCode,
item.ActivityCode,
item.code,
item.value,
item.name,
item.label,
])
: cleanString(item);
const optionLabel =
item && typeof item === "object"
? firstNonEmptyString([
item.activityName,
item.ActivityName,
item.meaning,
item.label,
item.name,
optionValue,
])
: optionValue;
const normalizedValue = cleanString(optionValue);
const normalizedLabel = cleanString(optionLabel);
if (!normalizedValue || seen.has(normalizedValue)) {
return null;
}
seen.add(normalizedValue);
return {
value: normalizedValue,
label: normalizedLabel || normalizedValue,
};
})
.filter(Boolean);
}
function collectActivityTextValues(value, seen = new WeakSet()) {
if (!value || typeof value !== "object") {
return [];
}
if (seen.has(value)) {
return [];
}
seen.add(value);
if (Array.isArray(value)) {
return value.flatMap((item) => collectActivityTextValues(item, seen));
}
const values = [];
const activityFieldNames = new Set([
normalizeString("SRActivities_c"),
normalizeString("srActivities_c"),
normalizeString("activities"),
]);
for (const [key, nestedValue] of Object.entries(value)) {
if (
activityFieldNames.has(normalizeString(key)) &&
typeof nestedValue !== "object"
) {
const text = cleanString(nestedValue);
if (text) {
values.push(text);
}
} else if (nestedValue && typeof nestedValue === "object") {
values.push(...collectActivityTextValues(nestedValue, seen));
}
}
return values;
}
function parseSemicolonActivityOptions(value) {
const seen = new Set();
return cleanString(value)
.split(";")
.map((item) => cleanString(item))
.filter(Boolean)
.map((item) => {
const normalizedItem = normalizeString(item);
if (seen.has(normalizedItem)) {
return null;
}
seen.add(normalizedItem);
return {
value: item,
label: item,
};
})
.filter(Boolean);
}
function parseActivityTextOptions(value) {
if (!value) {
return [];
}
try {
const parsed = JSON.parse(value);
if (Array.isArray(parsed)) {
return parsed;
}
if (parsed && typeof parsed === "object") {
return extractArray(parsed, ["items", "data", "results", "content", "activities"]);
}
} catch {
// Continue with delimiter based parsing.
}
return value
.split(/[;|\n]/)
.map((item) => item.trim())
.filter(Boolean);
}
function normalizeTaskTypeRequest(payload) {
return extractArray(payload, ["items", "data", "results", "content"])
.map((item) => {
const value = cleanString(
getFieldValue(item, ["lookUpCode_c", "LookUpCode_c", "lookupCode"])
);
const label = cleanString(
getFieldValue(item, [
"lookUpMeaning_c",
"LookUpMeaning_c",
"lookupMeaning",
"meaning",
])
);
const tags = cleanString(
getFieldValue(item, ["tags_c", "Tags_c", "TAGS_C", "tags", "Tags"])
);
return {
value,
label: label || value,
tags,
};
})
.filter((item) => item.value);
}
function getTimeManagementTaskTypeOptions(modal, srNumber) {
const options = filterTaskTypesForServiceRequest(modal.taskTypes || [], srNumber);
return options.length
? options
: [{ value: "", label: "No task types available", tags: "" }];
}
function filterTaskTypesForServiceRequest(taskTypes, srNumber) {
const expectedTag = isServiceRequestSrNumber(srNumber) ? "SERVICE" : "NON_SERVICE";
return (taskTypes || []).filter((item) => {
const tags = normalizeString(item?.tags);
return tags === normalizeString(expectedTag);
});
}
function isServiceRequestSrNumber(srNumber) {
return /^SR\d+$/i.test(cleanString(srNumber));
}
function isNonServiceRequestSrNumber(srNumber) {
return normalizeString(srNumber) === normalizeString("non-service-sr");
}
function getTimeManagementActivityOptions(modal, srNumber) {
const normalizedSrNumber = normalizeString(srNumber);
const matchedSrKey = Object.keys(modal.activityOptionsBySr || {}).find(
(key) => normalizeString(key) === normalizedSrNumber
);
const options =
modal.activityOptionsBySr?.[srNumber] ||
modal.activityOptionsBySr?.[matchedSrKey] ||
[];
return options.length
? options
: [{ value: "", label: "No activities available" }];
}
function findTimeManagementServiceRequest(modal, inputValue) {
const normalizedInput = normalizeString(inputValue);
return (modal.serviceRequests || []).find((item) => {
return (
normalizeString(item.srNumber) === normalizedInput ||
normalizeString(item.label) === normalizedInput ||
normalizeString(item.srTitle) === normalizedInput
);
});
}
function findTimeManagementServiceRequestByNumber(modal, srNumber) {
const normalizedSrNumber = normalizeString(srNumber);
return (modal.serviceRequests || []).find(
(item) => normalizeString(item.srNumber) === normalizedSrNumber
);
}
function findOptionValue(options, inputValue) {
const normalizedInput = normalizeString(inputValue);
const option = (options || []).find((item) => {
return (
normalizeString(item.value) === normalizedInput ||
normalizeString(item.label) === normalizedInput
);
});
return option?.value || "";
}
function findOptionLabel(options, value) {
const normalizedValue = normalizeString(value);
const option = (options || []).find(
(item) => normalizeString(item.value) === normalizedValue
);
return option?.label || "";
}
function buildServiceRequestActivityQuery(srNumbers) {
const values = [...(srNumbers || [])]
.map((srNumber) => cleanString(srNumber))
.filter(Boolean)
.map((srNumber) => `'${srNumber.replace(/'/g, "''")}'`)
.concat("'non-service-sr'")
.join(",");
return `SrNumber in (${values})`;
}
function closeDetailModal() {
if (appState.detailModal?.scope === "calendar-day" && appState.detailModal.value) {
appState.pendingFocusSelector = getCalendarDayFocusSelector(
appState.detailModal.value
);
}
appState.detailModal = null;
appState.timeEntriesDrawer = null;
appState.timeManagementModal = null;
appState.rampComparisonModal = null;
appState.forecastUpdateConfirmModal = null;
}
function shiftCalendarPeriod(delta) {
const nextDate =
appState.calendarView === "week"
? getShiftedCalendarWeekDate(delta)
: new Date(appState.calendarYear, appState.calendarMonth + delta, 1);
appState.calendarYear = nextDate.getFullYear();
appState.calendarMonth = nextDate.getMonth();
appState.calendarAnchorDateKey = getLocalDateKey(nextDate);
renderModal();
}
function goToCurrentCalendarPeriod() {
const today = new Date();
appState.calendarYear = today.getFullYear();
appState.calendarMonth = today.getMonth();
appState.calendarAnchorDateKey = getLocalDateKey(today);
renderModal();
}
function setCalendarView(view) {
const normalizedView = view === "week" ? "week" : "month";
if (appState.calendarView === normalizedView) {
return;
}
appState.calendarView = normalizedView;
setStoredCalendarView(normalizedView);
if (normalizedView === "week") {
const today = new Date();
const anchorDate =
today.getFullYear() === appState.calendarYear &&
today.getMonth() === appState.calendarMonth
? today
: new Date(appState.calendarYear, appState.calendarMonth, 1);
appState.calendarAnchorDateKey = getLocalDateKey(anchorDate);
appState.calendarYear = anchorDate.getFullYear();
appState.calendarMonth = anchorDate.getMonth();
} else {
const anchorDate = getCalendarAnchorDate(
appState.calendarYear,
appState.calendarMonth,
appState.calendarAnchorDateKey
);
appState.calendarYear = anchorDate.getFullYear();
appState.calendarMonth = anchorDate.getMonth();
}
renderModal();
}
function getShiftedCalendarWeekDate(delta) {
const anchorDate = getCalendarAnchorDate(
appState.calendarYear,
appState.calendarMonth,
appState.calendarAnchorDateKey
);
anchorDate.setDate(anchorDate.getDate() + delta * 7);
return anchorDate;
}
function shiftCalendarMonth(delta) {
shiftCalendarPeriod(delta);
}
function goToCurrentCalendarMonth() {
goToCurrentCalendarPeriod();
}
async function submitActionForm(form) {
if (!appState.dataset || !appState.actionFormModal) {
return;
}
if (!form.reportValidity()) {
return;
}
const formData = new FormData(form);
const parentId = cleanString(formData.get("parentId"));
const ownerEmail = cleanString(formData.get("ownerEmail"));
const startDate = toIsoDateString(formData.get("startDate"));
const endDate = toIsoDateString(formData.get("endDate"));
const payload = {
parentType: "WORKLOAD",
parentId,
action: {
ownerEmail,
role: "CLOUD_ARCHITECT",
name: "SUCCESS_PLAN_CONSUM_RVW",
team: cleanString(formData.get("team")),
tags: "",
startDate,
endDate,
status: "COMPLETE",
complexity: cleanString(formData.get("complexity")),
notes: [],
objective: "",
createdBy: cleanString(formData.get("createdBy")),
updatedBy: cleanString(formData.get("updatedBy")),
},
};
try {
appState.actionFormModal = {
...appState.actionFormModal,
isSubmitting: true,
errorMessage: "",
};
renderModal();
await fetchJson(API_ENDPOINTS.workloadActions, {
method: "POST",
headers: {
"content-type": "application/json; charset=UTF-8",
},
body: JSON.stringify(payload),
});
await applyCreatedActionToDataset(payload);
appState.actionFormModal = null;
renderModal();
} catch (error) {
appState.actionFormModal = {
...appState.actionFormModal,
isSubmitting: false,
errorMessage: getErrorMessage(error),
};
renderModal();
}
}
async function applyCreatedActionToDataset(payload) {
if (!appState.dataset) {
return;
}
const updatedCustomers = (appState.dataset.customers || []).map((customer) => ({
...customer,
workloads: (customer.workloads || []).map((workload) => {
if (workload.workloadId !== payload.parentId) {
return workload;
}
return {
...workload,
hasAction: true,
actions: [
...(workload.actions || []),
{
owner: payload.action.ownerEmail,
role: payload.action.role,
name: payload.action.name,
team: payload.action.team,
startDate: payload.action.startDate,
endDate: payload.action.endDate,
complexity: payload.action.complexity,
},
],
};
}),
}));
const exportPayload = createExportPayload(
appState.dataset.user,
updatedCustomers,
appState.dataset.exportPayload?.pendingServiceRequests || [],
appState.dataset.exportPayload?.resourceCurrentUser || null
);
const nextDataset = createDatasetSnapshot(exportPayload);
const previousDetailModal = appState.detailModal
? {
scope: appState.detailModal.scope,
value: appState.detailModal.value,
sortKey: appState.detailModal.sortKey,
sortDirection: appState.detailModal.sortDirection,
}
: null;
appState.dataset = nextDataset;
await writeCachedDataset(exportPayload);
if (previousDetailModal?.scope) {
restoreDetailModal(previousDetailModal);
}
}
function restoreDetailModal(detailState) {
const eligibleWorkloads = appState.dataset?.summary?.eligibleWorkloads || [];
let items = [];
let label = "";
let title = "";
if (detailState.scope === "stage") {
items = eligibleWorkloads.filter(
(workload) => workload.opportunityForecastTypeGroup === detailState.value
);
label = `Opportunity type: ${detailState.value}`;
title = `${detailState.value} workloads`;
} else if (detailState.scope === "sr") {
const flag = detailState.value === "true";
items = eligibleWorkloads.filter((workload) => workload.hasSR === flag);
label = "Status de SRs";
title = flag ? "Workloads com SR" : "Workloads sem SR";
} else if (detailState.scope === "action") {
const flag = detailState.value === "true";
items = eligibleWorkloads.filter((workload) => workload.hasAction === flag);
label = "Status de Consumption Plan/action";
title = flag ? "Workloads com action" : "Workloads sem action";
} else if (detailState.scope === "calendar-day") {
const allWorkloads = appState.dataset?.summary?.allWorkloads || [];
items = allWorkloads.filter((workload) => {
return getLocalDateKey(workload.consumptionStartDate) === detailState.value;
});
label = "Workload start calendar";
title = `Workloads starting ${formatDate(detailState.value) || detailState.value}`;
}
appState.detailModal = {
label,
title,
items,
totalAcr: items.reduce((sum, workload) => sum + workload.adjustedACR, 0),
sortKey: detailState.sortKey || DETAIL_SORT_KEYS.adjustedACR,
sortDirection: detailState.sortDirection || "desc",
scope: detailState.scope,
value: detailState.value,
type: "workloads",
};
}
function toggleDetailSort(sortKey) {
if (!appState.detailModal) {
return;
}
const nextDirection =
appState.detailModal.sortKey === sortKey &&
appState.detailModal.sortDirection === "asc"
? "desc"
: "asc";
appState.detailModal = {
...appState.detailModal,
sortKey,
sortDirection: nextDirection,
};
renderModal();
}
function getSortedDetailItems() {
if (!appState.detailModal) {
return [];
}
return sortWorkloads(
appState.detailModal.items,
appState.detailModal.sortKey,
appState.detailModal.sortDirection
);
}
function sortWorkloads(items, sortKey, sortDirection = "asc") {
const factor = sortDirection === "desc" ? -1 : 1;
return [...items].sort((left, right) => {
const comparison = compareWorkloadValues(
getSortableWorkloadValue(left, sortKey),
getSortableWorkloadValue(right, sortKey)
);
if (comparison !== 0) {
return comparison * factor;
}
return compareWorkloadValues(
getSortableWorkloadValue(left, DETAIL_SORT_KEYS.workload),
getSortableWorkloadValue(right, DETAIL_SORT_KEYS.workload)
);
});
}
function getSortableWorkloadValue(workload, sortKey) {
switch (sortKey) {
case DETAIL_SORT_KEYS.opportunityNumber:
return cleanString(workload.opportunityNumber);
case DETAIL_SORT_KEYS.customerName:
return cleanString(workload.customerName);
case DETAIL_SORT_KEYS.workload:
return `${cleanString(workload.name)}|${cleanString(workload.description)}`;
case DETAIL_SORT_KEYS.rampMonths:
return toNumber(workload.rampMonths);
case DETAIL_SORT_KEYS.adjustedACR:
return toNumber(workload.adjustedACR);
case DETAIL_SORT_KEYS.opportunityForecastTypeGroup:
return cleanString(workload.opportunityForecastTypeGroup);
case DETAIL_SORT_KEYS.hasSR:
return Boolean(workload.hasSR);
case DETAIL_SORT_KEYS.hasAction:
return Boolean(workload.hasAction);
default:
return cleanString(workload.name);
}
}
function compareWorkloadValues(left, right) {
if (typeof left === "number" || typeof right === "number") {
return toNumber(left) - toNumber(right);
}
if (typeof left === "boolean" || typeof right === "boolean") {
return Number(Boolean(left)) - Number(Boolean(right));
}
return String(left || "").localeCompare(String(right || ""), undefined, {
numeric: true,
sensitivity: "base",
});
}
function downloadFile(filename, content, contentType) {
const blob = new Blob([content], { type: contentType });
const url = URL.createObjectURL(blob);
const anchor = document.createElement("a");
anchor.href = url;
anchor.download = filename;
anchor.click();
window.setTimeout(() => {
URL.revokeObjectURL(url);
}, 1000);
}
function createExportHtml(dataset) {
const json = JSON.stringify(dataset.exportPayload);
return `<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Arch Panel Dashboard</title>
<style>
:root {
--bg: #0f141b;
--panel: #171e27;
--panel-strong: #202a36;
--border: #33404f;
--text: #f4f7fb;
--text-muted: #a8b4c2;
--accent: #7dc6e7;
--accent-soft: rgba(125, 198, 231, 0.14);
--success: #61bf81;
--warning: #ef8d67;
}
* { box-sizing: border-box; }
body {
margin: 0;
background: linear-gradient(180deg, #0d1218, #121923 45%, #10161e 100%);
color: var(--text);
font: 14px/1.45 "Segoe UI", Arial, sans-serif;
}
.shell {
max-width: 1480px;
margin: 0 auto;
padding: 28px;
}
.hero, .panel {
border: 1px solid var(--border);
background: var(--panel);
border-radius: 18px;
}
.hero {
display: flex;
justify-content: space-between;
gap: 24px;
padding: 24px;
margin-bottom: 18px;
}
.eyebrow {
margin: 0 0 8px;
color: var(--text-muted);
font-size: 11px;
font-weight: 700;
letter-spacing: .08em;
text-transform: uppercase;
}
h1, h2, h3, p { margin: 0; }
h1 { font-size: 34px; line-height: 1.1; }
.subtitle { margin-top: 12px; color: var(--text-muted); max-width: 720px; }
.pill {
display: inline-flex;
align-items: center;
min-height: 32px;
padding: 0 12px;
border-radius: 999px;
background: var(--accent-soft);
color: var(--text);
border: 1px solid rgba(255,255,255,.08);
font-weight: 600;
}
.grid {
display: grid;
gap: 16px;
}
.metrics { grid-template-columns: repeat(4, minmax(0,1fr)); margin-bottom: 18px; }
.metric, .boolean-card {
border: 1px solid var(--border);
background: var(--panel);
border-radius: 16px;
padding: 18px;
}
.metric-label { color: var(--text-muted); font-size: 12px; }
.metric-value { margin-top: 8px; font-size: 28px; font-weight: 700; }
.metric-detail { margin-top: 6px; color: var(--text-muted); font-size: 12px; }
.indicator-grid { grid-template-columns: repeat(4, minmax(0,1fr)); margin-bottom: 18px; }
.status-grid { grid-template-columns: repeat(2, minmax(0,1fr)); margin-bottom: 18px; }
.boolean-grid { display: grid; grid-template-columns: repeat(2, minmax(0,1fr)); gap: 12px; margin-top: 14px; }
.progress { display: grid; gap: 8px; margin-bottom: 12px; }
.progress-track {
display: flex;
width: 100%;
height: 10px;
overflow: hidden;
border-radius: 999px;
background: rgba(255,255,255,.06);
border: 1px solid var(--border);
}
.progress-segment { height: 100%; }
.progress-segment.is-success { background: linear-gradient(90deg, #7fd29a, #3fa35c); }
.progress-segment.is-danger { background: linear-gradient(90deg, #f2a28d, #d94f2b); }
.progress-labels {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
color: var(--text-muted);
font-size: 11px;
font-weight: 600;
}
.boolean-card {
appearance: none;
color: var(--text);
text-align: left;
cursor: pointer;
}
.boolean-card strong { display: block; margin-top: 8px; font-size: 28px; }
.layout { grid-template-columns: repeat(2, minmax(0, 1fr)); align-items: start; }
.panel { padding: 18px; }
.panel-head { display: flex; justify-content: space-between; gap: 16px; margin-bottom: 14px; }
.panel-title { margin-top: 6px; font-size: 18px; }
table { width: 100%; border-collapse: collapse; }
.main-table { table-layout: fixed; }
.main-table col.col-opportunity { width: 14%; }
.main-table col.col-customer { width: 16%; }
.main-table col.col-workload { width: auto; }
.main-table col.col-acr { width: 11%; }
.main-table col.col-type { width: 11%; }
.main-table col.col-flag { width: 9%; }
th, td { padding: 12px 14px; border-bottom: 1px solid var(--border); font-size: 12px; white-space: nowrap; }
th { color: var(--text-muted); text-align: left; background: var(--panel-strong); }
td { color: var(--text); }
td.is-numeric, th.is-numeric { text-align: right; }
.main-table th:nth-child(3),
.main-table td:nth-child(3) {
white-space: normal;
overflow-wrap: anywhere;
word-break: break-word;
}
.main-table td:nth-child(3) a {
display: block;
}
.main-table th:nth-child(2),
.main-table td:nth-child(2) {
white-space: normal;
overflow-wrap: anywhere;
}
tr:hover td { background: rgba(255,255,255,.02); }
a { color: var(--accent); text-decoration: none; }
.status {
display: inline-flex;
align-items: center;
min-height: 22px;
padding: 0 8px;
border-radius: 999px;
font-size: 11px;
font-weight: 700;
}
.status.true { color: #d9f9e4; background: rgba(97,191,129,.2); }
.status.false { color: #ffd8cc; background: rgba(239,141,103,.18); }
.row-stack { display: grid; gap: 12px; }
.row {
display: grid;
gap: 8px;
padding: 12px 0;
border-top: 1px solid var(--border);
}
.row:first-child { border-top: 0; padding-top: 0; }
.row-top {
display: flex;
justify-content: space-between;
gap: 12px;
}
.row-label { font-weight: 700; }
.row-sub { color: var(--text-muted); font-size: 12px; }
.bar { position: relative; height: 8px; border-radius: 999px; overflow: hidden; background: rgba(255,255,255,.08); }
.bar > span { position: absolute; inset: 0 auto 0 0; border-radius: inherit; background: linear-gradient(90deg, var(--accent), #4b8ce2); }
.sr-table td { white-space: normal; vertical-align: top; }
.sr-main { width: 78%; }
.sr-side { width: 22%; text-align: right; }
.sr-line { display: flex; flex-wrap: wrap; gap: 10px 14px; margin-top: 6px; }
.sr-line.head { margin-top: 0; }
.sr-customer { color: var(--text); font-size: 13px; font-weight: 700; }
.sr-moment { color: var(--text); font-size: 18px; font-weight: 700; }
.sr-exact, .sr-hours { margin-top: 6px; color: var(--text-muted); font-size: 12px; }
.empty {
padding: 18px;
border: 1px dashed var(--border);
border-radius: 8px;
color: var(--text-muted);
font-size: 13px;
}
.overlay {
position: fixed;
inset: 0;
display: none;
align-items: center;
justify-content: center;
padding: 24px;
background: rgba(8, 12, 18, .78);
backdrop-filter: blur(10px);
}
.overlay.open { display: flex; }
.detail {
width: min(1480px, calc(100vw - 32px));
max-height: calc(100vh - 32px);
overflow: auto;
border: 1px solid var(--border);
background: var(--panel);
border-radius: 18px;
padding: 18px;
}
.detail-head {
display: flex;
justify-content: space-between;
gap: 16px;
align-items: center;
margin-bottom: 12px;
}
.close {
appearance: none;
min-height: 34px;
padding: 0 14px;
border: 1px solid var(--border);
border-radius: 8px;
background: var(--panel-strong);
color: var(--text);
cursor: pointer;
}
.detail-actions {
display: flex;
justify-content: flex-end;
position: sticky;
bottom: 0;
margin-top: 16px;
padding-top: 12px;
background: linear-gradient(180deg, rgba(23, 30, 39, 0), var(--panel) 35%);
}
.detail table { table-layout: fixed; }
.detail th, .detail td {
white-space: normal;
vertical-align: top;
overflow-wrap: anywhere;
}
.detail-actions {
display: flex;
justify-content: flex-end;
margin-top: 16px;
}
.sort-button {
appearance: none;
padding: 0;
border: 0;
background: transparent;
color: inherit;
font: inherit;
font-weight: 600;
cursor: pointer;
}
.sort-button.is-numeric {
width: 100%;
text-align: right;
}
.clickable {
appearance: none;
width: 100%;
color: inherit;
text-align: left;
cursor: pointer;
}
@media (max-width: 1100px) {
.metrics, .indicator-grid, .status-grid, .layout { grid-template-columns: 1fr; }
}
</style>
</head>
<body>
<div id="app"></div>
<div id="detailOverlay" class="overlay"></div>
<script>
const payload = ${json};
const allowedTypes = ["SQL", "Pipeline", "Upside", "Forecast"];
const detailSortKeys = {
opportunityNumber: "opportunityNumber",
customerName: "customerName",
workload: "workload",
adjustedACR: "adjustedACR",
opportunityForecastTypeGroup: "opportunityForecastTypeGroup",
hasSR: "hasSR",
hasAction: "hasAction"
};
let detailState = null;
function normalizeForecastType(value) {
const text = String(value || "").trim();
return text ? text.split(" -")[0].trim() || "Unknown" : "Unknown";
}
function formatWholeNumber(value) {
return new Intl.NumberFormat("en-US", { maximumFractionDigits: 0 }).format(value);
}
function formatCurrency(value) {
return new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
maximumFractionDigits: 0
}).format(value || 0);
}
function formatPercent(value) {
return new Intl.NumberFormat("en-US", {
maximumFractionDigits: 1
}).format(value || 0) + "%";
}
function formatRelativeMoment(value) {
if (!value) {
return "-";
}
const target = new Date(value);
if (Number.isNaN(target.getTime())) {
return String(value);
}
let remaining = Math.max(Date.now() - target.getTime(), 0);
const day = 24 * 60 * 60 * 1000;
const hour = 60 * 60 * 1000;
const minute = 60 * 1000;
const second = 1000;
const days = Math.floor(remaining / day);
remaining -= days * day;
const hours = Math.floor(remaining / hour);
remaining -= hours * hour;
const minutes = Math.floor(remaining / minute);
remaining -= minutes * minute;
const seconds = Math.floor(remaining / second);
return days + "d " + hours + "h " + minutes + "m " + seconds + "s";
}
function flattenWorkloads() {
return (payload.customers || []).flatMap((customer) =>
(customer.workloads || []).map((workload) => ({
...workload,
customerName: customer.name
}))
);
}
function buildSummary(workloads) {
const eligible = workloads
.map((workload) => ({
...workload,
opportunityForecastTypeGroup: normalizeForecastType(workload.opportunityForecastTypeGroup || workload.opportunityForecastType)
}))
.filter((workload) => allowedTypes.includes(workload.opportunityForecastTypeGroup))
.sort((a, b) => (b.adjustedACR || 0) - (a.adjustedACR || 0));
return {
eligible,
stageIndicators: allowedTypes.map((label) => {
const items = eligible.filter((workload) => workload.opportunityForecastTypeGroup === label);
return {
label,
count: items.length,
totalAcr: items.reduce((sum, item) => sum + (item.adjustedACR || 0), 0)
};
}),
sr: {
trueCount: eligible.filter((item) => item.hasSR).length,
falseCount: eligible.filter((item) => !item.hasSR).length
},
action: {
trueCount: eligible.filter((item) => item.hasAction).length,
falseCount: eligible.filter((item) => !item.hasAction).length
}
};
}
function opportunityLink(workload) {
if (!workload.opportunityId || !workload.opportunityNumber) {
return workload.opportunityNumber || "Opportunity";
}
const href = "https://eeho.fa.us2.oraclecloud.com/fscmUI/redwood/cx-sales/application/container/opportunities/opportunities-detail?id=" +
encodeURIComponent(workload.opportunityId) +
"&puid=" +
encodeURIComponent(workload.opportunityNumber);
return '<a href="' + href + '" target="_blank" rel="noreferrer">' + workload.opportunityNumber + '</a>';
}
function workloadLink(workload) {
const label = (workload.name || "Unnamed workload") + (workload.description ? " | " + workload.description : "");
if (!workload.workloadId) {
return label;
}
const href = "https://spa.oracle.com/oalcrm/web/api/g2m-consumer-application/ui/index.html?ojr=workload_workbench/workload_details;workloadId=" +
encodeURIComponent(workload.workloadId);
return '<a href="' + href + '" target="_blank" rel="noreferrer">' + label + '</a>';
}
function statusToken(value) {
const label = value === "true" ? "Sim" : "Não";
return '<span class="status ' + value + '">' + label + '</span>';
}
function renderTable(items) {
return '<table class="main-table"><colgroup><col class="col-opportunity"><col class="col-customer"><col class="col-workload"><col class="col-acr"><col class="col-type"><col class="col-flag"><col class="col-flag"></colgroup><thead><tr><th>Opportunity</th><th>Customer</th><th>Workload</th><th class="is-numeric">ACR</th><th>Type</th><th>Tem SR?</th><th>Tem Action?</th></tr></thead><tbody>' +
items.map((workload) => '<tr><td>' + opportunityLink(workload) + '</td><td>' + (workload.customerName || '') + '</td><td>' + workloadLink(workload) + '</td><td class="is-numeric">' + formatCurrency(workload.adjustedACR) + '</td><td>' + workload.opportunityForecastTypeGroup + '</td><td>' + statusToken(String(Boolean(workload.hasSR))) + '</td><td>' + statusToken(String(Boolean(workload.hasAction))) + '</td></tr>').join('') +
'</tbody></table>';
}
function getSortableValue(workload, sortKey) {
if (sortKey === detailSortKeys.opportunityNumber) {
return String(workload.opportunityNumber || "");
}
if (sortKey === detailSortKeys.customerName) {
return String(workload.customerName || "");
}
if (sortKey === detailSortKeys.workload) {
return String((workload.name || "") + "|" + (workload.description || ""));
}
if (sortKey === detailSortKeys.adjustedACR) {
return Number(workload.adjustedACR || 0);
}
if (sortKey === detailSortKeys.opportunityForecastTypeGroup) {
return String(workload.opportunityForecastTypeGroup || "");
}
if (sortKey === detailSortKeys.hasSR) {
return Boolean(workload.hasSR);
}
if (sortKey === detailSortKeys.hasAction) {
return Boolean(workload.hasAction);
}
return String(workload.name || "");
}
function compareSortableValues(left, right) {
if (typeof left === "number" || typeof right === "number") {
return Number(left || 0) - Number(right || 0);
}
if (typeof left === "boolean" || typeof right === "boolean") {
return Number(Boolean(left)) - Number(Boolean(right));
}
return String(left || "").localeCompare(String(right || ""), undefined, {
numeric: true,
sensitivity: "base"
});
}
function sortItems(items, sortKey, sortDirection) {
const factor = sortDirection === "desc" ? -1 : 1;
return items.slice().sort((left, right) => {
const comparison = compareSortableValues(
getSortableValue(left, sortKey),
getSortableValue(right, sortKey)
);
if (comparison !== 0) {
return comparison * factor;
}
return compareSortableValues(
getSortableValue(left, detailSortKeys.workload),
getSortableValue(right, detailSortKeys.workload)
);
});
}
function renderSortButton(label, sortKey, isNumeric) {
const direction = detailState && detailState.sortKey === sortKey
? (detailState.sortDirection === "asc" ? " ↑" : " ↓")
: "";
return '<button class="sort-button' + (isNumeric ? ' is-numeric' : '') + '" data-sort-key="' + sortKey + '">' + label + direction + '</button>';
}
function renderDetailTable(items) {
return '<table><thead><tr><th>' + renderSortButton('Opportunity', detailSortKeys.opportunityNumber, false) + '</th><th>' + renderSortButton('Customer', detailSortKeys.customerName, false) + '</th><th>' + renderSortButton('Workload', detailSortKeys.workload, false) + '</th><th class="is-numeric">' + renderSortButton('ACR', detailSortKeys.adjustedACR, true) + '</th><th>' + renderSortButton('Type', detailSortKeys.opportunityForecastTypeGroup, false) + '</th><th>' + renderSortButton('HasSR', detailSortKeys.hasSR, false) + '</th><th>' + renderSortButton('HasAction', detailSortKeys.hasAction, false) + '</th></tr></thead><tbody>' +
items.map((workload) => '<tr><td>' + opportunityLink(workload) + '</td><td>' + (workload.customerName || '') + '</td><td>' + workloadLink(workload) + '</td><td class="is-numeric">' + formatCurrency(workload.adjustedACR) + '</td><td>' + workload.opportunityForecastTypeGroup + '</td><td>' + statusToken(String(Boolean(workload.hasSR))) + '</td><td>' + statusToken(String(Boolean(workload.hasAction))) + '</td></tr>').join('') +
'</tbody></table>';
}
function openDetail(title, label, items) {
const totalAcr = items.reduce((sum, item) => sum + (item.adjustedACR || 0), 0);
const overlay = document.getElementById("detailOverlay");
overlay.innerHTML = '<div class="detail"><div class="detail-head"><div><p class="eyebrow">' + label + '</p><h2>' + title + '</h2></div><button class="close" id="closeDetail">Close</button></div><p class="subtitle">' + formatWholeNumber(items.length) + ' workloads · Total (ACR): ' + formatCurrency(totalAcr) + '</p><div style="margin-top:16px">' + renderTable(items) + '</div></div>';
overlay.classList.add("open");
document.getElementById("closeDetail").addEventListener("click", () => overlay.classList.remove("open"));
overlay.onclick = (event) => {
if (event.target === overlay) {
overlay.classList.remove("open");
}
};
}
function renderSortButtonStateful(label, sortKey, isNumeric) {
const direction = detailState && detailState.sortKey === sortKey
? (detailState.sortDirection === "asc" ? " ^" : " v")
: "";
return '<button class="sort-button' + (isNumeric ? ' is-numeric' : '') + '" data-sort-key="' + sortKey + '">' + label + direction + '</button>';
}
function renderDetailTableStateful(items) {
return '<table><thead><tr><th>' + renderSortButtonStateful('Opportunity', detailSortKeys.opportunityNumber, false) + '</th><th>' + renderSortButtonStateful('Customer', detailSortKeys.customerName, false) + '</th><th>' + renderSortButtonStateful('Workload', detailSortKeys.workload, false) + '</th><th class="is-numeric">' + renderSortButtonStateful('ACR', detailSortKeys.adjustedACR, true) + '</th><th>' + renderSortButtonStateful('Type', detailSortKeys.opportunityForecastTypeGroup, false) + '</th><th>' + renderSortButtonStateful('Tem SR?', detailSortKeys.hasSR, false) + '</th><th>' + renderSortButtonStateful('Tem Action?', detailSortKeys.hasAction, false) + '</th></tr></thead><tbody>' +
items.map((workload) => '<tr><td>' + opportunityLink(workload) + '</td><td>' + (workload.customerName || '') + '</td><td>' + workloadLink(workload) + '</td><td class="is-numeric">' + formatCurrency(workload.adjustedACR) + '</td><td>' + workload.opportunityForecastTypeGroup + '</td><td>' + statusToken(String(Boolean(workload.hasSR))) + '</td><td>' + statusToken(String(Boolean(workload.hasAction))) + '</td></tr>').join('') +
'</tbody></table>';
}
function openDetailStateful(title, label, items) {
detailState = {
title: title,
label: label,
items: items.slice(),
sortKey: detailSortKeys.adjustedACR,
sortDirection: "desc"
};
renderDetailStateful();
}
function renderDetailStateful() {
if (!detailState) {
return;
}
const totalAcr = detailState.items.reduce((sum, item) => sum + (item.adjustedACR || 0), 0);
const sortedItems = sortItems(detailState.items, detailState.sortKey, detailState.sortDirection);
const overlay = document.getElementById("detailOverlay");
overlay.innerHTML = '<div class="detail"><div class="detail-head"><div><p class="eyebrow">' + detailState.label + '</p><h2>' + detailState.title + '</h2></div><button class="close" id="closeDetail">Fechar</button></div><p class="subtitle">' + formatWholeNumber(sortedItems.length) + ' workloads · Total (ACR): ' + formatCurrency(totalAcr) + '</p><div style="margin-top:16px">' + renderDetailTableStateful(sortedItems) + '</div><div class="detail-actions"><button class="close" id="closeDetailFooter">Fechar</button></div></div>';
overlay.classList.add("open");
const close = () => {
overlay.classList.remove("open");
detailState = null;
};
document.getElementById("closeDetail").addEventListener("click", close);
document.getElementById("closeDetailFooter").addEventListener("click", close);
overlay.querySelectorAll("[data-sort-key]").forEach((button) => {
button.addEventListener("click", () => {
const sortKey = button.getAttribute("data-sort-key");
const nextDirection = detailState.sortKey === sortKey && detailState.sortDirection === "asc"
? "desc"
: "asc";
detailState.sortKey = sortKey;
detailState.sortDirection = nextDirection;
renderDetailStateful();
});
});
overlay.onclick = (event) => {
if (event.target === overlay) {
close();
}
};
}
function render() {
const workloads = flattenWorkloads();
const summary = buildSummary(workloads);
const app = document.getElementById("app");
app.innerHTML = '<div class="shell">' +
'<section class="hero"><div><p class="eyebrow">Live Oracle dataset</p><h1>Arch panel export</h1><p class="subtitle">Generated from user, customer, workload, action and service request APIs.</p></div><div><span class="pill">' + formatCurrency(summary.eligible.reduce((sum, item) => sum + (item.adjustedACR || 0), 0)) + ' total eligible ACR</span></div></section>' +
'<section class="grid metrics">' +
'<article class="metric"><p class="metric-label">Total clients</p><p class="metric-value">' + formatWholeNumber(payload.customers.length) + '</p><p class="metric-detail">Loaded from listSummary</p></article>' +
'<article class="metric"><p class="metric-label">Total workloads</p><p class="metric-value">' + formatWholeNumber(workloads.length) + '</p><p class="metric-detail">Won workloads excluded upstream</p></article>' +
'<article class="metric"><p class="metric-label">Current user</p><p class="metric-value" style="font-size:18px">' + (payload.user.name || payload.user.userEmail) + '</p><p class="metric-detail">' + payload.user.userEmail + '</p></article>' +
'</section>' +
'<section class="grid indicator-grid">' +
summary.stageIndicators.map((indicator) => '<button class="metric clickable" data-kind="stage" data-value="' + indicator.label + '"><p class="metric-label">' + indicator.label + '</p><p class="metric-value">' + formatWholeNumber(indicator.count) + '</p><p class="metric-detail">Total (ACR): ' + formatCurrency(indicator.totalAcr) + '</p></button>').join('') +
'</section>' +
'<section class="grid status-grid">' +
'<article class="panel"><p class="eyebrow">Status de SRs</p><div class="progress"><div class="progress-track"><span class="progress-segment is-success" style="width:' + ((summary.sr.trueCount + summary.sr.falseCount) > 0 ? (summary.sr.trueCount / (summary.sr.trueCount + summary.sr.falseCount)) * 100 : 0) + '%"></span><span class="progress-segment is-danger" style="width:' + ((summary.sr.trueCount + summary.sr.falseCount) > 0 ? (summary.sr.falseCount / (summary.sr.trueCount + summary.sr.falseCount)) * 100 : 0) + '%"></span></div><div class="progress-labels"><span>Workloads com SR: ' + formatPercent((summary.sr.trueCount + summary.sr.falseCount) > 0 ? (summary.sr.trueCount / (summary.sr.trueCount + summary.sr.falseCount)) * 100 : 0) + '</span><span>Workloads sem SR: ' + formatPercent((summary.sr.trueCount + summary.sr.falseCount) > 0 ? (summary.sr.falseCount / (summary.sr.trueCount + summary.sr.falseCount)) * 100 : 0) + '</span></div></div><div class="boolean-grid"><button class="boolean-card" data-kind="sr" data-value="true"><span>Workloads com SR</span><strong>' + formatWholeNumber(summary.sr.trueCount) + '</strong></button><button class="boolean-card" data-kind="sr" data-value="false"><span>Workloads sem SR</span><strong>' + formatWholeNumber(summary.sr.falseCount) + '</strong></button></div></article>' +
'<article class="panel"><p class="eyebrow">Status de Consumption Plan/action</p><div class="progress"><div class="progress-track"><span class="progress-segment is-success" style="width:' + ((summary.action.trueCount + summary.action.falseCount) > 0 ? (summary.action.trueCount / (summary.action.trueCount + summary.action.falseCount)) * 100 : 0) + '%"></span><span class="progress-segment is-danger" style="width:' + ((summary.action.trueCount + summary.action.falseCount) > 0 ? (summary.action.falseCount / (summary.action.trueCount + summary.action.falseCount)) * 100 : 0) + '%"></span></div><div class="progress-labels"><span>Workloads com action: ' + formatPercent((summary.action.trueCount + summary.action.falseCount) > 0 ? (summary.action.trueCount / (summary.action.trueCount + summary.action.falseCount)) * 100 : 0) + '</span><span>Workloads sem action: ' + formatPercent((summary.action.trueCount + summary.action.falseCount) > 0 ? (summary.action.falseCount / (summary.action.trueCount + summary.action.falseCount)) * 100 : 0) + '</span></div></div><div class="boolean-grid"><button class="boolean-card" data-kind="action" data-value="true"><span>Workloads com action</span><strong>' + formatWholeNumber(summary.action.trueCount) + '</strong></button><button class="boolean-card" data-kind="action" data-value="false"><span>Workloads sem action</span><strong>' + formatWholeNumber(summary.action.falseCount) + '</strong></button></div></article>' +
'</section>' +
'</div>';
app.querySelectorAll("[data-kind]").forEach((element) => {
element.addEventListener("click", () => {
const kind = element.getAttribute("data-kind");
const value = element.getAttribute("data-value");
let items = [];
let title = "";
let label = "";
if (kind === "stage") {
items = summary.eligible.filter((item) => item.opportunityForecastTypeGroup === value);
label = "Opportunity type: " + value;
title = value + " workloads";
} else if (kind === "sr") {
const flag = value === "true";
items = summary.eligible.filter((item) => Boolean(item.hasSR) === flag);
label = "Status de SRs";
title = flag ? "Workloads com SR" : "Workloads sem SR";
} else if (kind === "action") {
const flag = value === "true";
items = summary.eligible.filter((item) => Boolean(item.hasAction) === flag);
label = "Status de Consumption Plan/action";
title = flag ? "Workloads com action" : "Workloads sem action";
}
openDetailStateful(title, label, items);
});
});
}
render();
</script>
</body>
</html>`;
}
function toggleTheme() {
appState.theme = appState.theme === "dark" ? "light" : "dark";
setStoredTheme(appState.theme);
renderModal();
}
function toggleSidebar() {
appState.sidebarCollapsed = !appState.sidebarCollapsed;
setStoredSidebarCollapsed(appState.sidebarCollapsed);
renderModal();
}
function applyDialogTheme(overlay) {
const dialog = overlay?.querySelector(`#${IDS.dialog}`);
if (!dialog) {
return;
}
dialog.setAttribute("data-theme", appState.theme);
}
function ensureStyles() {
if (document.getElementById(IDS.style)) {
return;
}
const style = document.createElement("style");
style.id = IDS.style;
style.textContent = `
html.${CLASSES.modalOpen},
body.${CLASSES.modalOpen} {
overflow: hidden !important;
}
.arch-panel-extension-entry {
--arch-panel-bg: rgba(255, 255, 255, 0.12);
--arch-panel-bg-hover: rgba(255, 255, 255, 0.2);
--arch-panel-border: rgba(255, 255, 255, 0.26);
--arch-panel-fg: #ffffff;
--arch-panel-focus: rgba(255, 255, 255, 0.72);
--arch-panel-font: inherit;
--arch-workbench-header: #4b335d;
--arch-workbench-accent: #6c4d80;
display: inline-flex;
align-items: center;
margin-inline-end: 12px;
flex: 0 0 auto;
}
.arch-panel-extension-button {
appearance: none;
border: 1px solid var(--arch-panel-border);
background: var(--arch-panel-bg);
color: var(--arch-panel-fg);
border-radius: 999px;
min-height: 36px;
padding: 0 14px;
display: inline-flex;
align-items: center;
justify-content: center;
font-family: var(--arch-panel-font);
font-size: 13px;
font-weight: 600;
line-height: 1;
letter-spacing: 0.01em;
cursor: pointer;
transition:
background-color 160ms ease,
border-color 160ms ease,
transform 160ms ease;
}
.arch-panel-extension-button:hover {
background: var(--arch-panel-bg-hover);
}
.arch-panel-extension-button:active {
transform: translateY(1px);
}
.arch-panel-extension-button:focus-visible,
.arch-panel-extension-close:focus-visible,
.arch-panel-extension-theme-toggle:focus-visible,
.arch-panel-extension-sidebar-button:focus-visible,
.arch-panel-extension-utility-button:focus-visible,
.arch-panel-extension-summary-card-button:focus-visible,
.arch-panel-extension-sort-button:focus-visible,
.arch-panel-extension-indicator-card:focus-visible,
.arch-panel-extension-row-link:focus-visible {
outline: 2px solid var(--arch-panel-focus);
outline-offset: 2px;
}
#${IDS.hcmTile} {
cursor: pointer;
}
#${IDS.hcmTile}:focus-visible {
outline: 2px solid #0b5cab;
outline-offset: 3px;
}
.arch-panel-extension-overlay {
position: fixed;
inset: 0;
z-index: 2147483647;
display: flex;
opacity: 0;
pointer-events: none;
background: rgba(13, 17, 24, 0.56);
backdrop-filter: blur(8px);
transition: opacity 160ms ease;
}
.arch-panel-extension-overlay.${CLASSES.overlayOpen} {
opacity: 1;
pointer-events: auto;
}
.arch-panel-extension-dialog {
width: 100vw;
height: 100vh;
display: grid;
grid-template-rows: auto 1fr;
--wb-app-bg: #f5f4f3;
--wb-panel: #ffffff;
--wb-panel-soft: #fafafa;
--wb-sidebar: #f3f2f1;
--wb-border: #d9d9d9;
--wb-border-strong: #c8c8c8;
--wb-row-divider: #e6e6e6;
--wb-divider: #ececec;
--wb-row-hover: #f9fbfc;
--wb-text: #1f1f1f;
--wb-text-secondary: #5f6670;
--wb-text-muted: #7b8189;
--wb-link: #006b9a;
--wb-info: #dceef7;
--wb-success: #3fa35c;
--wb-warning: #d94f2b;
--wb-shadow: 0 1px 2px rgba(31, 31, 31, 0.04);
--arch-workbench-header-fallback: #3f2d4f;
--arch-workbench-accent-fallback: #684b78;
--arch-shell-header-text: #ffffff;
--arch-shell-header-muted: rgba(255, 255, 255, 0.82);
--arch-shell-header-subtle: rgba(255, 255, 255, 0.68);
background: var(--wb-app-bg);
color: var(--wb-text);
font-family: "Oracle Sans", "Segoe UI", Arial, sans-serif;
}
.arch-panel-extension-dialog[data-theme="dark"] {
--wb-app-bg: #141b24;
--wb-panel: #1f2833;
--wb-panel-soft: #273240;
--wb-sidebar: #19222c;
--wb-border: #364150;
--wb-border-strong: #566171;
--wb-row-divider: #333d4b;
--wb-divider: #303a46;
--wb-row-hover: #273240;
--wb-text: #f4f7fb;
--wb-text-secondary: #c6d0da;
--wb-text-muted: #9ca8b5;
--wb-link: #84c7e3;
--wb-info: #233847;
--wb-success: #5ec280;
--wb-warning: #f0946c;
--wb-shadow: none;
--arch-workbench-header-fallback: #202a36;
--arch-workbench-accent-fallback: #39495b;
}
.arch-panel-extension-dialog * {
box-sizing: border-box;
}
.arch-panel-extension-shell-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 24px;
min-height: 64px;
padding: 12px 24px;
background:
linear-gradient(
135deg,
var(--arch-workbench-header, var(--arch-workbench-header-fallback)) 0%,
var(--arch-workbench-accent, var(--arch-workbench-accent-fallback)) 100%
);
border-bottom: 1px solid rgba(255, 255, 255, 0.16);
box-shadow: 0 8px 24px rgba(31, 31, 31, 0.16);
color: var(--arch-shell-header-text);
}
.arch-panel-extension-brand {
display: flex;
align-items: center;
gap: 14px;
min-width: 0;
}
.arch-panel-extension-brand-mark {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 40px;
height: 40px;
border-radius: 6px;
background: rgba(255, 255, 255, 0.18);
color: var(--arch-shell-header-text);
font-size: 14px;
font-weight: 700;
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.16);
}
.arch-panel-extension-brand-label {
margin: 0 0 2px;
color: var(--arch-shell-header-muted);
font-size: 11px;
letter-spacing: 0.08em;
text-transform: uppercase;
}
.arch-panel-extension-title {
margin: 0;
color: var(--arch-shell-header-text);
font-size: 24px;
line-height: 1.2;
font-weight: 600;
letter-spacing: -0.01em;
}
.arch-panel-extension-shell-actions {
display: flex;
align-items: center;
gap: 12px;
flex: 0 0 auto;
}
.arch-panel-extension-shell-note {
max-width: 36vw;
overflow: hidden;
color: var(--arch-shell-header-muted);
font-size: 12px;
line-height: 1.35;
text-overflow: ellipsis;
white-space: nowrap;
}
.arch-panel-extension-theme-toggle {
appearance: none;
min-height: 34px;
padding: 0 14px;
border: 1px solid rgba(255, 255, 255, 0.28);
border-radius: 4px;
background: rgba(255, 255, 255, 0.12);
color: var(--arch-shell-header-text);
font: inherit;
font-size: 13px;
font-weight: 600;
cursor: pointer;
transition: background-color 160ms ease, border-color 160ms ease, transform 160ms ease;
}
.arch-panel-extension-shell-header .arch-panel-extension-theme-toggle:hover {
border-color: rgba(255, 255, 255, 0.44);
background: rgba(255, 255, 255, 0.2);
}
.arch-panel-extension-close {
appearance: none;
min-height: 34px;
padding: 0 14px;
border: 1px solid var(--wb-border);
border-radius: 4px;
background: var(--wb-panel-soft);
color: var(--wb-text);
font: inherit;
font-size: 13px;
font-weight: 600;
cursor: pointer;
box-shadow: none;
}
.arch-panel-extension-close:hover {
border-color: var(--wb-border-strong);
background: var(--wb-row-hover);
}
.arch-panel-extension-shell-header .arch-panel-extension-close {
border-color: rgba(255, 255, 255, 0.28);
background: rgba(255, 255, 255, 0.12);
color: var(--arch-shell-header-text);
}
.arch-panel-extension-shell-header .arch-panel-extension-close:hover {
border-color: rgba(255, 255, 255, 0.42);
background: rgba(255, 255, 255, 0.16);
}
.arch-panel-extension-detail-dialog .arch-panel-extension-close {
border-color: var(--wb-border);
background: var(--wb-panel-soft);
color: var(--wb-text);
box-shadow: none;
}
.arch-panel-extension-sidebar-button,
.arch-panel-extension-utility-button {
appearance: none;
min-height: 34px;
padding: 0 14px;
border: 1px solid var(--wb-border);
border-radius: 4px;
background: var(--wb-panel);
color: var(--wb-text);
font: inherit;
font-size: 13px;
font-weight: 600;
cursor: pointer;
}
.arch-panel-extension-utility-button.is-primary {
border-color: #0b5cab;
background: #0b5cab;
color: #ffffff;
text-shadow: 0 1px 0 rgba(0, 0, 0, 0.16);
box-shadow: 0 8px 18px rgba(11, 92, 171, 0.24);
}
.arch-panel-extension-utility-button.is-primary:hover {
border-color: #084a8a;
background: #084a8a;
}
.arch-panel-extension-utility-button.is-primary:disabled {
border-color: var(--wb-border);
background: var(--wb-panel-soft);
color: var(--wb-text-muted);
box-shadow: none;
cursor: not-allowed;
}
.arch-panel-extension-shell-body {
display: grid;
grid-template-columns: 260px minmax(0, 1fr);
min-height: 0;
position: relative;
}
.arch-panel-extension-shell-body.is-sidebar-collapsed {
grid-template-columns: minmax(0, 1fr);
}
.arch-panel-extension-sidebar {
background: var(--wb-sidebar);
border-right: 1px solid var(--wb-border);
padding: 20px 14px;
}
.arch-panel-extension-sidebar.is-collapsed {
display: none;
}
.arch-panel-extension-sidebar-group + .arch-panel-extension-sidebar-group {
margin-top: 18px;
padding-top: 18px;
border-top: 1px solid var(--wb-border);
}
.arch-panel-extension-sidebar-label,
.arch-panel-extension-page-eyebrow,
.arch-panel-extension-panel-label,
.arch-panel-extension-state-eyebrow {
margin: 0 0 8px;
color: var(--wb-text-muted);
font-size: 11px;
font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
}
.arch-panel-extension-sidebar-data {
display: grid;
gap: 6px;
color: var(--wb-text-secondary);
font-size: 12px;
}
.arch-panel-extension-sidebar-data strong {
color: var(--wb-text);
font-size: 13px;
}
.arch-panel-extension-current-user-card {
display: grid;
justify-items: start;
gap: 10px;
}
.arch-panel-extension-current-user-avatar {
width: 48px;
height: 48px;
border: 1px solid var(--wb-border);
border-radius: 999px;
background: var(--wb-panel);
box-shadow: var(--wb-shadow);
color: var(--wb-text);
object-fit: cover;
}
.arch-panel-extension-current-user-avatar.is-fallback {
display: inline-grid;
place-items: center;
background: var(--arch-workbench-header, var(--arch-workbench-header-fallback));
color: #ffffff;
font-size: 15px;
font-weight: 700;
letter-spacing: 0.02em;
}
.arch-panel-extension-sidebar-button {
width: 100%;
justify-content: center;
display: inline-flex;
align-items: center;
border-color: var(--wb-border);
background: var(--wb-panel);
color: var(--wb-text);
}
.arch-panel-extension-sidebar-button + .arch-panel-extension-sidebar-button {
margin-top: 8px;
}
.arch-panel-extension-main-shell {
display: flex;
flex-direction: column;
min-width: 0;
overflow: auto;
overflow-anchor: none;
background: var(--wb-app-bg);
padding: 20px 24px 24px;
}
.arch-panel-extension-main-shell:has(.arch-panel-extension-state-card) {
grid-column: 1 / -1;
align-items: stretch;
justify-content: center;
min-height: min(720px, calc(100vh - 120px));
}
.arch-panel-extension-page-hero {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 24px;
margin-bottom: 18px;
}
.arch-panel-extension-page-title,
.arch-panel-extension-state-title {
margin: 0;
color: var(--wb-text);
font-size: 34px;
line-height: 1.15;
font-weight: 700;
}
.arch-panel-extension-page-subtitle,
.arch-panel-extension-state-copy {
margin: 10px 0 0;
max-width: 760px;
color: var(--wb-text-secondary);
font-size: 13px;
line-height: 1.55;
}
.arch-panel-extension-hero-aside {
display: grid;
gap: 8px;
justify-items: end;
}
.arch-panel-extension-hero-pill {
display: inline-flex;
align-items: center;
min-height: 28px;
padding: 0 10px;
border: 1px solid var(--wb-border);
border-radius: 14px;
background: var(--wb-info);
color: var(--wb-text);
font-size: 12px;
font-weight: 600;
}
.arch-panel-extension-hero-note {
color: var(--wb-text-secondary);
font-size: 12px;
}
.arch-panel-extension-summary-row,
.arch-panel-extension-indicator-grid,
.arch-panel-extension-status-grid,
.arch-panel-extension-content-grid,
.arch-panel-extension-boolean-grid {
display: grid;
gap: 14px;
}
.arch-panel-extension-summary-row {
grid-template-columns: repeat(4, minmax(0, 1fr));
margin-bottom: 16px;
}
.arch-panel-extension-indicator-grid {
grid-template-columns: repeat(4, minmax(0, 1fr));
margin-bottom: 16px;
}
.arch-panel-extension-status-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
margin-bottom: 16px;
}
.arch-panel-extension-content-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.arch-panel-extension-summary-card,
.arch-panel-extension-panel,
.arch-panel-extension-indicator-card,
.arch-panel-extension-state-card {
border: 1px solid var(--wb-border);
border-radius: 8px;
background: var(--wb-panel);
box-shadow: var(--wb-shadow);
}
.arch-panel-extension-summary-card,
.arch-panel-extension-state-card,
.arch-panel-extension-indicator-card {
padding: 16px;
}
.arch-panel-extension-state-card {
width: 100%;
min-height: 320px;
display: flex;
flex-direction: column;
justify-content: center;
padding: 34px 40px;
}
.arch-panel-extension-loading-progress {
display: grid;
gap: 10px;
width: 100%;
margin-top: 30px;
}
.arch-panel-extension-loading-progress-meta {
display: flex;
align-items: center;
justify-content: flex-start;
gap: 16px;
color: var(--wb-text-secondary);
font-size: 12px;
font-weight: 700;
}
.arch-panel-extension-loading-progress-track {
position: relative;
height: 30px;
overflow: hidden;
border: 1px solid color-mix(in srgb, var(--wb-accent) 50%, var(--wb-border-strong));
border-radius: 8px;
background: color-mix(in srgb, var(--wb-panel-soft) 68%, #000000);
}
.arch-panel-extension-loading-progress-fill {
position: absolute;
inset: 0 auto 0 0;
width: 0;
min-width: 0;
border-radius: inherit;
background: linear-gradient(90deg, #1f7a4d, #2fbf71);
transition: width 520ms ease;
}
.arch-panel-extension-loading-progress-value {
position: absolute;
inset: 0 auto 0 50%;
transform: translateX(-50%);
display: flex;
align-items: center;
justify-content: center;
min-width: 54px;
margin: 4px 0;
padding: 0 8px;
border: 1px solid rgba(255, 255, 255, 0.26);
border-radius: 999px;
background: rgba(7, 12, 18, 0.72);
color: #ffffff;
font-size: 12px;
font-weight: 800;
line-height: 1;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.28);
text-shadow: none;
pointer-events: none;
}
.arch-panel-extension-summary-card-button {
appearance: none;
width: 100%;
text-align: left;
color: inherit;
font: inherit;
line-height: inherit;
cursor: pointer;
}
.arch-panel-extension-panel {
padding: 16px;
}
.arch-panel-extension-summary-label,
.arch-panel-extension-summary-detail,
.arch-panel-extension-indicator-label,
.arch-panel-extension-indicator-subtitle,
.arch-panel-extension-indicator-total,
.arch-panel-extension-insight-detail,
.arch-panel-extension-stage-detail {
margin: 0;
}
.arch-panel-extension-summary-label,
.arch-panel-extension-indicator-label {
color: var(--wb-text-secondary);
font-size: 12px;
}
.arch-panel-extension-summary-value,
.arch-panel-extension-indicator-value {
margin: 8px 0 4px;
color: var(--wb-text);
font-size: 26px;
line-height: 1.2;
font-weight: 700;
}
.arch-panel-extension-summary-detail,
.arch-panel-extension-indicator-subtitle,
.arch-panel-extension-indicator-total {
color: var(--wb-text-muted);
font-size: 11px;
}
.arch-panel-extension-indicator-card {
appearance: none;
text-align: left;
color: inherit;
cursor: pointer;
}
.arch-panel-extension-boolean-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
margin-top: 12px;
}
.arch-panel-extension-progress {
display: grid;
gap: 8px;
margin-bottom: 12px;
}
.arch-panel-extension-progress-track {
display: flex;
width: 100%;
height: 10px;
overflow: hidden;
border-radius: 999px;
background: var(--wb-panel-soft);
border: 1px solid var(--wb-border);
}
.arch-panel-extension-progress-segment {
height: 100%;
}
.arch-panel-extension-progress-segment.is-success {
background: linear-gradient(90deg, #7fd29a, #3fa35c);
}
.arch-panel-extension-progress-segment.is-danger {
background: linear-gradient(90deg, #f2a28d, #d94f2b);
}
.arch-panel-extension-progress-labels {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
color: var(--wb-text-secondary);
font-size: 11px;
font-weight: 600;
}
.arch-panel-extension-panel-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
margin-bottom: 14px;
}
.arch-panel-extension-panel-title {
margin: 0;
color: var(--wb-text);
font-size: 16px;
font-weight: 600;
}
.arch-panel-extension-panel-meta {
display: flex;
align-items: center;
gap: 8px;
color: var(--wb-text-muted);
font-size: 12px;
}
.arch-panel-extension-operations-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 14px;
}
.arch-panel-extension-sr-hour-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 14px;
align-content: start;
}
.arch-panel-extension-calendar-card {
min-width: 0;
}
.arch-panel-extension-calendar-actions {
display: inline-flex;
align-items: center;
gap: 8px;
color: var(--wb-text-muted);
font-size: 12px;
}
.arch-panel-extension-calendar-view-toggle {
display: inline-flex;
align-items: center;
overflow: hidden;
border: 1px solid var(--wb-border);
border-radius: 4px;
background: var(--wb-panel-soft);
}
.arch-panel-extension-calendar-view-button {
appearance: none;
min-height: 28px;
padding: 0 9px;
border: 0;
border-right: 1px solid var(--wb-border);
background: transparent;
color: var(--wb-text-secondary);
font: inherit;
font-size: 12px;
font-weight: 700;
cursor: pointer;
}
.arch-panel-extension-calendar-view-button:last-child {
border-right: 0;
}
.arch-panel-extension-calendar-view-button:hover {
background: var(--wb-row-hover);
color: var(--wb-text);
}
.arch-panel-extension-calendar-view-button.is-active {
background: #0b5cab;
color: #ffffff;
}
.arch-panel-extension-calendar-nav {
appearance: none;
display: inline-flex;
align-items: center;
justify-content: center;
width: 28px;
height: 28px;
border: 1px solid var(--wb-border);
border-radius: 4px;
background: var(--wb-panel-soft);
color: var(--wb-text);
font-size: 18px;
line-height: 1;
cursor: pointer;
}
.arch-panel-extension-calendar-nav:hover {
border-color: var(--wb-accent);
color: var(--wb-accent);
}
.arch-panel-extension-calendar-today {
appearance: none;
min-height: 28px;
padding: 0 10px;
border: 1px solid var(--wb-border);
border-radius: 4px;
background: var(--wb-panel-soft);
color: var(--wb-text);
font: inherit;
font-size: 12px;
font-weight: 700;
cursor: pointer;
}
.arch-panel-extension-calendar-today:hover {
border-color: var(--wb-accent);
color: var(--wb-accent);
}
.arch-panel-extension-calendar-weekdays,
.arch-panel-extension-calendar-grid {
display: grid;
grid-template-columns: repeat(7, minmax(0, 1fr));
}
.arch-panel-extension-calendar-weekdays {
gap: 6px;
margin-bottom: 6px;
color: var(--wb-text-muted);
font-size: 11px;
font-weight: 700;
text-transform: uppercase;
}
.arch-panel-extension-calendar-weekdays span {
padding: 0 6px;
}
.arch-panel-extension-calendar-grid {
gap: 6px;
}
.arch-panel-extension-calendar-grid.is-week {
min-height: 300px;
}
.arch-panel-extension-calendar-day {
appearance: none;
position: relative;
display: block;
width: 100%;
text-align: left;
min-height: 74px;
overflow: hidden;
border: 1px solid var(--wb-border);
border-radius: 6px;
background: var(--wb-panel-soft);
padding: 7px;
font: inherit;
cursor: pointer;
}
.arch-panel-extension-calendar-day.is-week-day {
display: flex;
flex-direction: column;
min-height: 300px;
padding: 0;
overflow: hidden;
}
.arch-panel-extension-calendar-week-day-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
min-height: 38px;
padding: 8px 9px;
border-bottom: 1px solid var(--wb-border);
background: color-mix(in srgb, var(--wb-panel) 72%, var(--wb-panel-soft));
}
.arch-panel-extension-calendar-day.is-week-day .arch-panel-extension-calendar-week-number {
position: static;
font-size: 9px;
opacity: 0.76;
}
.arch-panel-extension-calendar-week-workloads {
display: flex;
flex: 1;
flex-direction: column;
gap: 7px;
padding: 8px;
overflow: auto;
}
.arch-panel-extension-calendar-week-workload {
display: block;
border-left: 3px solid #d94f2b;
border-radius: 5px;
background: rgba(217, 79, 43, 0.14);
padding: 7px 8px;
color: var(--wb-text);
text-align: left;
}
.arch-panel-extension-calendar-week-workload-title,
.arch-panel-extension-calendar-week-workload-meta {
display: block;
overflow: hidden;
text-overflow: ellipsis;
}
.arch-panel-extension-calendar-week-workload-title {
font-size: 11px;
font-weight: 700;
line-height: 1.25;
}
.arch-panel-extension-calendar-week-workload-meta {
margin-top: 3px;
color: var(--wb-text-secondary);
font-size: 10px;
line-height: 1.2;
white-space: nowrap;
}
.arch-panel-extension-calendar-week-empty {
margin: auto;
color: var(--wb-text-muted);
font-size: 11px;
font-weight: 600;
}
.arch-panel-extension-calendar-day:disabled {
cursor: default;
}
.arch-panel-extension-calendar-day.has-workload:hover {
border-color: #f26c4f;
background: rgba(217, 79, 43, 0.22);
}
.arch-panel-extension-calendar-day.is-muted {
opacity: 0.42;
}
.arch-panel-extension-calendar-day.is-today {
border-color: var(--wb-accent);
box-shadow: inset 0 0 0 1px var(--wb-accent);
}
.arch-panel-extension-calendar-day.has-workload {
border-color: rgba(217, 79, 43, 0.82);
background: rgba(217, 79, 43, 0.14);
}
.arch-panel-extension-calendar-date {
color: var(--wb-text);
font-size: 12px;
font-weight: 800;
}
.arch-panel-extension-calendar-week-number {
position: absolute;
right: 7px;
bottom: 6px;
color: var(--wb-text-muted);
font-size: 9px;
font-weight: 800;
letter-spacing: 0.04em;
opacity: 0.72;
pointer-events: none;
}
.arch-panel-extension-calendar-workload {
display: block;
margin-top: 8px;
color: var(--wb-text);
font-size: 11px;
line-height: 1.25;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
.arch-panel-extension-calendar-count {
position: absolute;
top: 6px;
right: 6px;
display: flex;
align-items: center;
justify-content: center;
min-width: 20px;
height: 20px;
padding: 0 6px;
border-radius: 999px;
background: #d94f2b;
color: #ffffff;
font-size: 11px;
font-weight: 800;
}
.arch-panel-extension-empty-state {
border: 1px dashed var(--wb-border);
border-radius: 8px;
padding: 16px;
color: var(--wb-text-muted);
font-size: 13px;
}
.arch-panel-extension-panel-dot {
width: 4px;
height: 4px;
border-radius: 50%;
background: var(--wb-border-strong);
}
.arch-panel-extension-table-wrap {
overflow: auto;
border: 1px solid var(--wb-border);
border-radius: 6px;
}
.arch-panel-extension-table-wrap.is-detail {
max-height: calc(100vh - 220px);
}
.arch-panel-extension-table-wrap.is-sr-list {
overflow: auto;
}
.arch-panel-extension-table-wrap.is-ramp-pivot {
max-height: min(460px, calc(100vh - 260px));
}
.arch-panel-extension-table {
width: 100%;
border-collapse: collapse;
background: var(--wb-panel);
}
.arch-panel-extension-table th,
.arch-panel-extension-table td {
padding: 12px 14px;
border-bottom: 1px solid var(--wb-row-divider);
font-size: 12px;
text-align: left;
white-space: nowrap;
}
.arch-panel-extension-table th {
background: var(--wb-panel-soft);
color: var(--wb-text-secondary);
font-weight: 600;
}
.arch-panel-extension-table td {
color: var(--wb-text);
}
.arch-panel-extension-ramp-pivot-table th:first-child {
min-width: 150px;
position: sticky;
left: 0;
z-index: 1;
}
.arch-panel-extension-ramp-pivot-table tbody th {
background: var(--wb-panel-soft);
color: var(--wb-text-secondary);
font-weight: 700;
}
.arch-panel-extension-ramp-pivot-table .is-delta-different {
color: #d94f2b;
font-weight: 800;
}
.arch-panel-extension-sr-table td {
white-space: normal;
vertical-align: top;
}
.arch-panel-extension-sr-main {
width: 78%;
}
.arch-panel-extension-sr-side {
width: 22%;
text-align: right;
}
.arch-panel-extension-sr-line {
display: flex;
flex-wrap: wrap;
gap: 10px 14px;
margin-top: 6px;
}
.arch-panel-extension-sr-line.is-head {
margin-top: 0;
}
.arch-panel-extension-sr-customer {
color: var(--wb-text);
font-size: 13px;
}
.arch-panel-extension-sr-moment {
color: var(--wb-text);
font-size: 18px;
font-weight: 700;
}
.arch-panel-extension-sr-exact,
.arch-panel-extension-sr-hours {
margin-top: 6px;
color: var(--wb-text-secondary);
font-size: 12px;
}
.arch-panel-extension-table-empty {
padding: 18px;
border: 1px dashed var(--wb-border);
border-radius: 8px;
color: var(--wb-text-secondary);
font-size: 13px;
}
.arch-panel-extension-table-wrap.is-main .arch-panel-extension-table {
table-layout: fixed;
}
.arch-panel-extension-table-wrap.is-main
.arch-panel-extension-col-opportunity {
width: 14%;
}
.arch-panel-extension-table-wrap.is-main
.arch-panel-extension-col-customer {
width: 16%;
}
.arch-panel-extension-table-wrap.is-main
.arch-panel-extension-col-workload {
width: auto;
}
.arch-panel-extension-table-wrap.is-main
.arch-panel-extension-col-acr {
width: 11%;
}
.arch-panel-extension-table-wrap.is-main
.arch-panel-extension-col-type {
width: 11%;
}
.arch-panel-extension-table-wrap.is-main
.arch-panel-extension-col-flag {
width: 9%;
}
.arch-panel-extension-table-wrap.is-main
.arch-panel-extension-table
th:nth-child(3),
.arch-panel-extension-table-wrap.is-main
.arch-panel-extension-table
td:nth-child(3) {
white-space: normal;
overflow-wrap: anywhere;
word-break: break-word;
}
.arch-panel-extension-table-wrap.is-main
.arch-panel-extension-table
td:nth-child(3)
.arch-panel-extension-row-link {
display: block;
}
.arch-panel-extension-table-wrap.is-main
.arch-panel-extension-table
th:nth-child(2),
.arch-panel-extension-table-wrap.is-main
.arch-panel-extension-table
td:nth-child(2) {
white-space: normal;
overflow-wrap: anywhere;
}
.arch-panel-extension-badge {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 54px;
min-height: 24px;
padding: 0 10px;
border-radius: 999px;
font-size: 11px;
font-weight: 700;
letter-spacing: 0.02em;
}
.arch-panel-extension-badge.is-success {
background: #dcfce7;
border: 1px solid #86efac;
color: #166534;
}
.arch-panel-extension-badge.is-danger {
background: #fee2e2;
border: 1px solid #fca5a5;
color: #991b1b;
}
.arch-panel-extension-action-cell {
display: inline-flex;
align-items: center;
gap: 8px;
}
.arch-panel-extension-add-action-button {
appearance: none;
display: inline-flex;
align-items: center;
justify-content: center;
width: 26px;
height: 26px;
padding: 0;
border: 1px solid var(--wb-border);
border-radius: 999px;
background: var(--wb-panel-soft);
color: var(--wb-text);
font: inherit;
font-size: 18px;
line-height: 1;
cursor: pointer;
}
.arch-panel-extension-table tbody tr:hover {
background: var(--wb-row-hover);
}
.arch-panel-extension-table th.is-numeric,
.arch-panel-extension-table td.is-numeric {
text-align: right;
}
.arch-panel-extension-row-link {
appearance: none;
padding: 0;
border: 0;
background: transparent;
color: var(--wb-link);
font: inherit;
font-size: 12px;
font-weight: 600;
cursor: pointer;
text-decoration: none;
}
.arch-panel-extension-workload-link-cell {
display: inline-flex;
align-items: center;
gap: 7px;
}
.arch-panel-extension-ramp-alert-button {
appearance: none;
display: inline-flex;
align-items: center;
justify-content: center;
width: 18px;
height: 18px;
padding: 0;
border: 1px solid #d94f2b;
border-radius: 999px;
background: #fee2e2;
color: #991b1b;
font: inherit;
font-size: 12px;
font-weight: 900;
line-height: 1;
cursor: pointer;
}
.arch-panel-extension-ramp-alert-button:hover {
background: #fecaca;
}
.arch-panel-extension-insight-list,
.arch-panel-extension-stage-list,
.arch-panel-extension-side-column {
display: grid;
gap: 14px;
}
.arch-panel-extension-insight {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 16px;
padding-top: 12px;
border-top: 1px solid var(--wb-divider);
}
.arch-panel-extension-insight:first-child {
padding-top: 0;
border-top: 0;
}
.arch-panel-extension-insight-label,
.arch-panel-extension-stage-label {
margin: 0 0 4px;
color: var(--wb-text);
font-size: 12px;
font-weight: 600;
}
.arch-panel-extension-insight-detail,
.arch-panel-extension-stage-detail {
color: var(--wb-text-muted);
font-size: 11px;
line-height: 1.45;
}
.arch-panel-extension-insight-value {
margin: 0;
color: var(--wb-text);
font-size: 16px;
font-weight: 700;
text-align: right;
}
.arch-panel-extension-stage-row {
display: grid;
gap: 8px;
}
.arch-panel-extension-stage-copy {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 12px;
}
.arch-panel-extension-stage-values {
display: grid;
justify-items: end;
gap: 2px;
color: var(--wb-text-secondary);
font-size: 12px;
}
.arch-panel-extension-stage-detail-row {
color: var(--wb-text-secondary);
font-size: 12px;
}
.arch-panel-extension-stage-bar {
position: relative;
height: 8px;
border-radius: 999px;
overflow: hidden;
background: color-mix(in srgb, var(--wb-text-secondary) 14%, transparent);
}
.arch-panel-extension-stage-bar span {
position: absolute;
inset: 0 auto 0 0;
border-radius: inherit;
background: linear-gradient(90deg, var(--wb-accent), color-mix(in srgb, var(--wb-accent) 52%, #ffffff));
}
.arch-panel-extension-detail-overlay {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
padding: 16px;
background: rgba(10, 14, 20, 0.68);
backdrop-filter: blur(6px);
}
.arch-panel-extension-time-drawer {
position: absolute;
top: 0;
right: 0;
bottom: 0;
z-index: 2;
width: min(560px, calc(100vw - 32px));
display: flex;
flex-direction: column;
gap: 14px;
padding: 18px;
border-left: 1px solid var(--wb-border);
background: var(--wb-panel);
box-shadow: -18px 0 42px rgba(0, 0, 0, 0.28);
overflow: auto;
animation: arch-panel-extension-slide-in 180ms ease-out;
}
.arch-panel-extension-time-drawer-head {
position: sticky;
top: -18px;
z-index: 1;
display: flex;
align-items: center;
justify-content: space-between;
gap: 14px;
padding: 0 0 12px;
border-bottom: 1px solid var(--wb-border);
background: var(--wb-panel);
}
.arch-panel-extension-time-drawer-close {
border-color: var(--wb-border);
background: var(--wb-panel-soft);
color: var(--wb-text);
box-shadow: none;
}
.arch-panel-extension-time-drawer-close:hover {
border-color: var(--wb-border-strong);
background: var(--wb-row-hover);
}
.arch-panel-extension-time-drawer-actions {
position: sticky;
bottom: -18px;
display: flex;
justify-content: flex-end;
margin: auto -18px -18px;
padding: 12px 18px 18px;
border-top: 1px solid var(--wb-border);
background: linear-gradient(180deg, rgba(23, 30, 39, 0), var(--wb-panel) 30%);
}
.arch-panel-extension-time-drawer-meta {
display: flex;
gap: 8px;
color: var(--wb-text-muted);
font-size: 12px;
}
.arch-panel-extension-time-drawer-state,
.arch-panel-extension-time-drawer-json {
border: 1px solid var(--wb-border);
border-radius: 8px;
background: var(--wb-panel-soft);
padding: 14px;
}
.arch-panel-extension-time-drawer-state.is-error {
border-color: rgba(217, 79, 43, 0.42);
}
.arch-panel-extension-time-entry-group {
display: grid;
gap: 10px;
}
.arch-panel-extension-time-entry-group + .arch-panel-extension-time-entry-group {
padding-top: 12px;
border-top: 1px solid var(--wb-border);
}
.arch-panel-extension-time-entry-group-head {
display: flex;
align-items: flex-end;
justify-content: space-between;
gap: 12px;
}
.arch-panel-extension-time-entry-group-head h4 {
margin: 3px 0 0;
color: var(--wb-text);
font-size: 16px;
line-height: 1.25;
}
.arch-panel-extension-time-entry-group-head span {
color: var(--wb-text-muted);
font-size: 12px;
white-space: nowrap;
}
.arch-panel-extension-time-entry-table-wrap {
overflow: auto;
border: 1px solid var(--wb-border);
border-radius: 8px;
}
.arch-panel-extension-time-entry-table {
width: 100%;
border-collapse: collapse;
background: var(--wb-panel);
}
.arch-panel-extension-time-entry-table th,
.arch-panel-extension-time-entry-table td {
padding: 10px 12px;
border-bottom: 1px solid var(--wb-row-divider);
color: var(--wb-text);
font-size: 12px;
text-align: left;
vertical-align: top;
}
.arch-panel-extension-time-entry-table th {
background: var(--wb-panel-soft);
color: var(--wb-text-secondary);
font-weight: 700;
}
.arch-panel-extension-time-entry-table tfoot th {
border-bottom: 0;
color: var(--wb-text);
}
.arch-panel-extension-time-entry-table .is-numeric {
text-align: right;
white-space: nowrap;
}
.arch-panel-extension-time-drawer-json summary {
color: var(--wb-link);
font-size: 12px;
font-weight: 700;
cursor: pointer;
}
.arch-panel-extension-time-drawer-json pre {
max-height: 320px;
margin: 12px 0 0;
overflow: auto;
color: var(--wb-text);
font-size: 11px;
line-height: 1.5;
white-space: pre-wrap;
}
@keyframes arch-panel-extension-slide-in {
from {
transform: translateX(100%);
}
to {
transform: translateX(0);
}
}
.arch-panel-extension-detail-dialog {
width: min(1520px, calc(100vw - 32px));
max-height: calc(100vh - 88px);
display: flex;
flex-direction: column;
overflow: hidden;
border: 1px solid var(--wb-border);
border-radius: 10px;
background: var(--wb-panel);
box-shadow: var(--wb-shadow);
padding: 18px;
}
.arch-panel-extension-ramp-dialog {
width: min(1320px, calc(100vw - 48px));
max-height: calc(100vh - 120px);
display: flex;
flex-direction: column;
overflow: hidden;
border: 1px solid var(--wb-border);
border-radius: 10px;
background: var(--wb-panel);
box-shadow: var(--wb-shadow);
padding: 18px;
}
.arch-panel-extension-action-dialog {
width: min(980px, calc(100vw - 32px));
max-height: calc(100vh - 88px);
overflow: auto;
border: 1px solid var(--wb-border);
border-radius: 10px;
background: var(--wb-panel);
box-shadow: var(--wb-shadow);
padding: 18px;
}
.arch-panel-extension-time-management-dialog {
width: min(1760px, calc(100vw - 20px));
max-height: calc(100vh - 88px);
display: flex;
flex-direction: column;
gap: 14px;
overflow: visible;
border: 1px solid var(--wb-border);
border-radius: 10px;
background: var(--wb-panel);
box-shadow: var(--wb-shadow);
padding: 18px;
}
.arch-panel-extension-time-management-toolbar,
.arch-panel-extension-time-management-card {
border: 1px solid var(--wb-border);
border-radius: 8px;
background: var(--wb-panel-soft);
padding: 14px;
}
.arch-panel-extension-time-management-card {
display: grid;
--arch-time-management-scrollbar-gutter: 16px;
gap: 4px;
min-height: 0;
overflow: visible;
}
.arch-panel-extension-time-management-card.is-error {
border-color: rgba(217, 79, 43, 0.42);
}
.arch-panel-extension-date-picker {
position: relative;
display: inline-flex;
align-items: center;
gap: 5px;
width: auto;
}
.arch-panel-extension-date-input-shell {
position: relative;
display: inline-flex;
align-items: center;
}
.arch-panel-extension-date-picker-button {
position: absolute;
top: 50%;
right: 11px;
z-index: 1;
display: inline-flex;
align-items: center;
justify-content: center;
width: 20px;
height: 20px;
border: 0;
border-radius: 5px;
background: transparent;
color: var(--wb-text-muted);
cursor: pointer;
font-size: 0;
line-height: 1;
padding: 0;
transform: translateY(-50%);
}
.arch-panel-extension-date-step-button {
display: inline-flex;
align-items: center;
justify-content: center;
width: 26px;
height: 32px;
border: 1px solid var(--wb-border);
border-radius: 7px;
background: var(--wb-panel);
color: var(--wb-text-muted);
cursor: pointer;
padding: 0;
}
.arch-panel-extension-date-step-button svg {
width: 16px;
height: 16px;
fill: currentColor;
pointer-events: none;
}
.arch-panel-extension-date-step-button:hover {
border-color: var(--wb-border-strong);
background: var(--wb-row-hover);
color: var(--wb-text);
}
.arch-panel-extension-date-picker-button svg {
width: 16px;
height: 16px;
fill: currentColor;
pointer-events: none;
}
.arch-panel-extension-date-picker-button:hover {
background: var(--wb-row-hover);
color: var(--wb-text);
}
.arch-panel-extension-date-picker-icon {
display: none;
position: absolute;
left: 11px;
z-index: 1;
color: var(--wb-text-muted);
font-size: 14px;
pointer-events: none;
}
.arch-panel-extension-date-picker input {
width: 118px;
min-height: 32px;
padding-left: 12px;
padding-right: 38px;
}
.arch-panel-extension-form-field .arch-panel-extension-date-picker input {
min-height: 32px;
padding: 0 38px 0 12px;
}
.arch-panel-extension-date-picker input[readonly] {
cursor: default;
}
.arch-panel-extension-date-picker-popover {
position: absolute;
top: calc(100% + 8px);
left: 0;
z-index: 6;
width: 292px;
border: 1px solid var(--wb-border);
border-radius: 10px;
background: var(--wb-panel);
box-shadow: 0 18px 42px rgba(0, 0, 0, 0.22);
padding: 12px;
}
.arch-panel-extension-date-picker-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
margin-bottom: 10px;
}
.arch-panel-extension-date-picker-head strong {
color: var(--wb-text);
font-size: 13px;
}
.arch-panel-extension-date-picker-weekdays,
.arch-panel-extension-date-picker-grid {
display: grid;
grid-template-columns: repeat(7, minmax(0, 1fr));
gap: 4px;
}
.arch-panel-extension-date-picker-weekdays {
margin-bottom: 5px;
color: var(--wb-text-muted);
font-size: 10px;
font-weight: 800;
text-align: center;
}
.arch-panel-extension-date-picker-day {
appearance: none;
min-width: 0;
height: 32px;
border: 1px solid transparent;
border-radius: 7px;
background: transparent;
color: var(--wb-text);
font: inherit;
font-size: 12px;
font-weight: 700;
cursor: pointer;
}
.arch-panel-extension-date-picker-day:hover {
border-color: var(--wb-border-strong);
background: var(--wb-row-hover);
}
.arch-panel-extension-date-picker-day.is-muted {
color: var(--wb-text-muted);
opacity: 0.55;
}
.arch-panel-extension-date-picker-day.is-today {
border-color: #0b5cab;
}
.arch-panel-extension-date-picker-day.is-selected {
border-color: #0b5cab;
background: #0b5cab;
color: #ffffff;
}
.arch-panel-extension-time-management-select-grid {
display: grid;
grid-template-columns: minmax(240px, 1.15fr) minmax(220px, 1fr) minmax(180px, 0.8fr);
gap: 12px;
}
.arch-panel-extension-time-management-rows {
display: grid;
gap: 10px;
max-height: min(52vh, 520px);
min-height: 0;
overflow-x: hidden;
overflow-y: auto;
padding-right: 0;
scrollbar-gutter: stable;
}
.arch-panel-extension-time-management-rows:focus-within {
overflow: visible;
}
.arch-panel-extension-time-management-header-row {
display: grid;
grid-template-columns:
minmax(420px, 3.8fr)
minmax(150px, 0.95fr)
minmax(138px, 0.82fr)
repeat(7, minmax(52px, 0.28fr))
34px;
align-items: center;
gap: 6px;
padding-right: var(--arch-time-management-scrollbar-gutter);
}
.arch-panel-extension-time-management-column-label {
color: var(--wb-text-secondary);
font-size: 12px;
font-weight: 600;
line-height: 1.2;
}
.arch-panel-extension-time-management-entry-row {
display: grid;
grid-template-columns:
minmax(420px, 3.8fr)
minmax(150px, 0.95fr)
minmax(138px, 0.82fr)
repeat(7, minmax(52px, 0.28fr))
34px;
align-items: start;
gap: 6px;
}
.arch-panel-extension-time-management-entry-row.is-compact {
align-items: center;
}
.arch-panel-extension-time-management-entry-row.is-compact .arch-panel-extension-form-field {
gap: 0;
}
.arch-panel-extension-combobox {
position: relative;
display: block;
}
.arch-panel-extension-combobox input {
min-height: 40px;
padding-left: 12px;
padding-right: 38px;
border-radius: 8px;
background: color-mix(in srgb, var(--wb-panel-soft) 86%, var(--wb-panel));
color: transparent;
transition:
border-color 140ms ease,
box-shadow 140ms ease,
background 140ms ease;
}
.arch-panel-extension-combobox input::placeholder {
color: transparent;
}
.arch-panel-extension-combobox:focus-within input::placeholder {
color: var(--wb-text-muted);
}
.arch-panel-extension-combobox-value {
position: absolute;
top: 50%;
right: 38px;
left: 12px;
z-index: 1;
overflow: hidden;
color: var(--wb-text);
font-size: 12px;
font-weight: 700;
line-height: 1.25;
pointer-events: none;
text-overflow: ellipsis;
transform: translateY(-50%);
white-space: nowrap;
}
.arch-panel-extension-combobox:focus-within .arch-panel-extension-combobox-value {
display: none;
}
.arch-panel-extension-combobox.is-disabled {
opacity: 0.72;
}
.arch-panel-extension-combobox.is-disabled .arch-panel-extension-combobox-value,
.arch-panel-extension-combobox.is-disabled .arch-panel-extension-combobox-arrow {
color: var(--wb-text-muted);
}
.arch-panel-extension-combobox.is-disabled input {
cursor: not-allowed;
}
.arch-panel-extension-combobox.is-disabled:focus-within .arch-panel-extension-combobox-menu {
display: none;
}
.arch-panel-extension-combobox input:focus {
border-color: #0b5cab;
background: var(--wb-panel);
box-shadow: 0 0 0 3px rgba(11, 92, 171, 0.16);
color: var(--wb-text);
outline: none;
}
.arch-panel-extension-combobox-arrow {
position: absolute;
top: 50%;
right: 12px;
z-index: 1;
display: inline-flex;
width: 16px;
height: 16px;
color: var(--wb-text-muted);
pointer-events: none;
transform: translateY(-50%);
}
.arch-panel-extension-combobox-arrow svg {
width: 100%;
height: 100%;
fill: currentColor;
}
.arch-panel-extension-combobox-menu {
position: absolute;
top: calc(100% + 6px);
left: 0;
right: 0;
z-index: 10000;
display: none;
max-height: 260px;
overflow: auto;
border: 1px solid var(--wb-border-strong);
border-radius: 10px;
background: var(--wb-panel);
box-shadow: 0 18px 42px rgba(0, 0, 0, 0.22);
padding: 6px;
}
.arch-panel-extension-combobox:focus-within .arch-panel-extension-combobox-menu {
display: grid;
gap: 4px;
}
.arch-panel-extension-combobox-option {
appearance: none;
display: grid;
gap: 2px;
position: relative;
width: 100%;
min-height: 38px;
border: 0;
border-radius: 8px;
background: transparent;
color: var(--wb-text);
cursor: pointer;
font: inherit;
line-height: 1.25;
padding: 8px 10px;
text-align: left;
}
.arch-panel-extension-combobox-option:hover,
.arch-panel-extension-combobox-option:focus,
.arch-panel-extension-combobox-option.is-active {
outline: none;
background: var(--wb-row-hover);
}
.arch-panel-extension-combobox-option.is-active {
box-shadow: inset 3px 0 0 #0b5cab;
}
.arch-panel-extension-combobox-option.is-selected {
background: rgba(11, 92, 171, 0.12);
color: var(--wb-text);
padding-right: 32px;
}
.arch-panel-extension-combobox-option.is-selected::after {
content: "✓";
position: absolute;
top: 50%;
right: 11px;
color: #0b5cab;
font-size: 14px;
font-weight: 800;
transform: translateY(-50%);
}
.arch-panel-extension-dialog[data-theme="dark"] .arch-panel-extension-combobox-option.is-selected::after {
color: #9ed4f0;
}
.arch-panel-extension-combobox-option-main {
color: var(--wb-text);
font-size: 12px;
font-weight: 700;
}
.arch-panel-extension-combobox-option-meta {
color: var(--wb-text-muted);
font-size: 11px;
font-weight: 600;
}
.arch-panel-extension-combobox-option[hidden] {
display: none;
}
.arch-panel-extension-combobox-empty {
color: var(--wb-text-muted);
font-size: 12px;
padding: 8px 9px;
}
.arch-panel-extension-time-management-days {
display: grid;
grid-template-columns: repeat(7, minmax(110px, 1fr));
gap: 8px;
}
.arch-panel-extension-time-management-day {
display: grid;
gap: 6px;
padding: 7px;
border: 1px solid var(--wb-border);
border-radius: 8px;
background: var(--wb-panel);
}
.arch-panel-extension-time-management-day-head {
display: grid;
align-content: center;
justify-items: center;
gap: 1px;
min-height: 22px;
padding: 0;
color: var(--wb-text-secondary);
line-height: 1.15;
text-align: center;
}
.arch-panel-extension-time-management-day-head strong {
color: var(--wb-text-secondary);
font-size: 12px;
font-weight: 600;
}
.arch-panel-extension-time-management-day-head span {
color: var(--wb-text-muted);
font-size: 11px;
font-weight: 600;
text-transform: none;
}
.arch-panel-extension-time-management-day input {
width: 100%;
min-height: 30px;
border: 1px solid var(--wb-border);
border-radius: 6px;
background: var(--wb-panel-soft);
color: var(--wb-text);
font: inherit;
font-size: 12px;
padding: 0 6px;
text-align: right;
}
.arch-panel-extension-time-management-delete {
appearance: none;
align-self: center;
display: inline-flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
border: 1px solid var(--wb-border);
border-radius: 7px;
background: var(--wb-panel);
color: var(--wb-text-muted);
cursor: pointer;
padding: 0;
}
.arch-panel-extension-time-management-delete:hover,
.arch-panel-extension-time-management-delete:focus-visible {
border-color: rgba(217, 79, 43, 0.52);
background: rgba(217, 79, 43, 0.12);
color: #d94f2b;
outline: none;
}
.arch-panel-extension-time-management-delete svg {
width: 16px;
height: 16px;
fill: currentColor;
pointer-events: none;
}
.arch-panel-extension-time-management-request {
border-top: 1px solid var(--wb-border);
padding-top: 10px;
}
.arch-panel-extension-time-management-request summary {
color: var(--wb-link);
cursor: pointer;
font-size: 12px;
font-weight: 700;
}
.arch-panel-extension-time-management-request p {
margin: 10px 0 4px;
color: var(--wb-text-secondary);
font-size: 11px;
font-weight: 800;
letter-spacing: 0.02em;
text-transform: uppercase;
}
.arch-panel-extension-time-management-request pre {
max-height: 220px;
margin: 8px 0 0;
overflow: auto;
border: 1px solid var(--wb-border);
border-radius: 7px;
background: var(--wb-panel);
color: var(--wb-text-secondary);
font-size: 11px;
line-height: 1.45;
padding: 10px;
white-space: pre-wrap;
word-break: break-all;
}
.arch-panel-extension-skeleton {
display: block;
overflow: hidden;
border-radius: 6px;
background: linear-gradient(
90deg,
color-mix(in srgb, var(--wb-border) 70%, transparent) 0%,
color-mix(in srgb, var(--wb-panel) 70%, #ffffff) 48%,
color-mix(in srgb, var(--wb-border) 70%, transparent) 100%
);
background-size: 220% 100%;
animation: arch-panel-extension-skeleton 1.15s ease-in-out infinite;
}
.arch-panel-extension-skeleton-control {
height: 38px;
}
.arch-panel-extension-skeleton-title {
width: 62%;
height: 14px;
}
.arch-panel-extension-skeleton-text {
width: 46%;
height: 11px;
}
.arch-panel-extension-skeleton-input {
height: 34px;
}
@keyframes arch-panel-extension-skeleton {
from {
background-position: 120% 0;
}
to {
background-position: -120% 0;
}
}
.arch-panel-extension-confirm-dialog {
width: min(560px, calc(100vw - 32px));
max-height: calc(100vh - 120px);
overflow: auto;
border: 1px solid var(--wb-border);
border-radius: 10px;
background: var(--wb-panel);
box-shadow: var(--wb-shadow);
padding: 18px;
}
.arch-panel-extension-detail-title {
margin: 0;
color: var(--wb-text);
font-size: 18px;
font-weight: 700;
}
.arch-panel-extension-detail-subtitle {
margin: 0 0 14px;
color: var(--wb-text-muted);
font-size: 12px;
line-height: 1.5;
overflow-wrap: anywhere;
}
.arch-panel-extension-detail-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
margin-bottom: 8px;
}
.arch-panel-extension-detail-meta {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 8px;
margin-bottom: 14px;
color: var(--wb-text-muted);
font-size: 12px;
}
.arch-panel-extension-detail-actions {
display: flex;
align-items: center;
justify-content: flex-end;
gap: 10px;
position: sticky;
bottom: 0;
margin-top: 16px;
padding-top: 12px;
background: linear-gradient(180deg, rgba(23, 30, 39, 0), var(--wb-panel) 35%);
}
.arch-panel-extension-time-management-footer-actions {
display: flex;
align-items: center;
gap: 8px;
margin-right: auto;
}
.arch-panel-extension-form-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 14px;
margin-top: 18px;
}
.arch-panel-extension-form-field {
display: grid;
gap: 6px;
}
.arch-panel-extension-form-field span {
color: var(--wb-text-secondary);
font-size: 12px;
font-weight: 600;
}
.arch-panel-extension-form-field input,
.arch-panel-extension-form-field select {
width: 100%;
min-height: 38px;
padding: 0 12px;
border: 1px solid var(--wb-border);
border-radius: 6px;
background: var(--wb-panel-soft);
color: var(--wb-text);
font: inherit;
}
.arch-panel-extension-form-field .arch-panel-extension-combobox input {
min-height: 40px;
padding: 0 38px 0 12px;
border-radius: 8px;
background: color-mix(in srgb, var(--wb-panel-soft) 86%, var(--wb-panel));
color: transparent;
}
.arch-panel-extension-form-field .arch-panel-extension-combobox input:focus {
border-color: #0b5cab;
background: var(--wb-panel);
box-shadow: 0 0 0 3px rgba(11, 92, 171, 0.16);
color: var(--wb-text);
outline: none;
}
.arch-panel-extension-form-error {
margin: 12px 0 0;
color: #d94f2b;
font-size: 12px;
font-weight: 600;
}
.arch-panel-extension-sort-button {
appearance: none;
display: inline-flex;
align-items: center;
gap: 4px;
padding: 0;
border: 0;
background: transparent;
color: inherit;
font: inherit;
font-weight: 600;
cursor: pointer;
}
.arch-panel-extension-sort-button.is-numeric {
justify-content: flex-end;
width: 100%;
}
.arch-panel-extension-table-wrap.is-detail {
flex: 1 1 auto;
min-height: 0;
max-height: calc(100vh - 270px);
overflow: auto;
}
.arch-panel-extension-table-wrap.is-detail .arch-panel-extension-table th {
position: sticky;
top: 0;
z-index: 3;
}
.arch-panel-extension-table-wrap.is-detail .arch-panel-extension-table thead {
position: relative;
z-index: 3;
}
.arch-panel-extension-table-wrap.is-detail .arch-panel-extension-table {
border-collapse: separate;
border-spacing: 0;
}
.arch-panel-extension-table-wrap.is-detail .arch-panel-extension-table {
table-layout: fixed;
}
.arch-panel-extension-table-wrap.is-detail .arch-panel-extension-table th,
.arch-panel-extension-table-wrap.is-detail .arch-panel-extension-table td {
white-space: normal;
vertical-align: top;
overflow-wrap: anywhere;
}
.arch-panel-extension-state-actions {
display: flex;
gap: 12px;
margin-top: 18px;
}
.arch-panel-extension-state-card.is-error {
border-color: rgba(217, 79, 43, 0.42);
}
@media (max-width: 1280px) {
.arch-panel-extension-summary-row,
.arch-panel-extension-indicator-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
@media (max-width: 1100px) {
.arch-panel-extension-shell-body,
.arch-panel-extension-content-grid,
.arch-panel-extension-status-grid,
.arch-panel-extension-operations-grid {
grid-template-columns: 1fr;
}
.arch-panel-extension-sidebar {
display: none;
}
}
@media (max-width: 780px) {
.arch-panel-extension-shell-header,
.arch-panel-extension-page-hero {
flex-direction: column;
align-items: flex-start;
}
.arch-panel-extension-summary-row,
.arch-panel-extension-indicator-grid,
.arch-panel-extension-boolean-grid,
.arch-panel-extension-sr-hour-grid {
grid-template-columns: 1fr;
}
.arch-panel-extension-state-card {
min-height: 260px;
padding: 26px 24px;
}
}
@media (max-width: 980px) {
.arch-panel-extension-action-dialog {
width: min(720px, calc(100vw - 24px));
}
.arch-panel-extension-form-grid {
grid-template-columns: 1fr;
}
}
`;
document.head.appendChild(style);
}
function applyTheme(header, functionsContainer, anchor, mount, overlay) {
const headerStyle = window.getComputedStyle(header);
const functionsStyle = window.getComputedStyle(functionsContainer);
const sourceBackground = resolveBackgroundColor(header, headerStyle);
const sourceForeground = resolveForegroundColor(
header,
functionsContainer,
anchor,
headerStyle,
functionsStyle
);
const workbenchHeader = mix(sourceBackground, { r: 75, g: 51, b: 93, a: 1 }, 0.52);
const workbenchAccent = mix(workbenchHeader, { r: 108, g: 77, b: 128, a: 1 }, 0.38);
const focusColor = mix(sourceForeground, workbenchHeader, 0.28);
const themeTargets = [mount, overlay].filter(Boolean);
for (const target of themeTargets) {
target.style.setProperty(
"--arch-panel-bg",
toColor(mix(sourceBackground, sourceForeground, 0.12))
);
target.style.setProperty(
"--arch-panel-bg-hover",
toColor(mix(sourceBackground, sourceForeground, 0.22))
);
target.style.setProperty(
"--arch-panel-border",
toColor(mix(sourceBackground, sourceForeground, 0.32))
);
target.style.setProperty("--arch-panel-fg", toColor(sourceForeground, 1));
target.style.setProperty("--arch-panel-focus", toColor(focusColor, 0.94));
target.style.setProperty(
"--arch-panel-font",
functionsStyle.fontFamily || headerStyle.fontFamily || "inherit"
);
target.style.setProperty("--arch-workbench-header", toColor(workbenchHeader, 1));
target.style.setProperty("--arch-workbench-accent", toColor(workbenchAccent, 1));
}
}
function resolveForegroundColor(
header,
functionsContainer,
anchor,
headerStyle,
functionsStyle
) {
const salesPlanningElement = findTextElement(header, "Sales Planning");
const salesPlanningColor = salesPlanningElement
? parseColor(window.getComputedStyle(salesPlanningElement).color)
: null;
const functionsItemColor = findVisibleChildColor(functionsContainer);
const userColor = anchor
? parseColor(window.getComputedStyle(anchor).color)
: null;
return (
userColor ||
functionsItemColor ||
parseColor(functionsStyle.color) ||
salesPlanningColor ||
parseColor(headerStyle.color) || {
r: 255,
g: 255,
b: 255,
a: 1,
}
);
}
function resolveBackgroundColor(element, style) {
const directBackground = parseColor(style.backgroundColor);
if (directBackground && directBackground.a > 0) {
return directBackground;
}
let current = element.parentElement;
while (current) {
const currentBackground = parseColor(
window.getComputedStyle(current).backgroundColor
);
if (currentBackground && currentBackground.a > 0) {
return currentBackground;
}
current = current.parentElement;
}
return { r: 75, g: 51, b: 93, a: 1 };
}
function parseColor(value) {
if (!value || value === "transparent") {
return null;
}
const rgbMatch = value.match(
/rgba?\(\s*(\d{1,3})[\s,]+(\d{1,3})[\s,]+(\d{1,3})(?:[\s,/]+([.\d]+))?\s*\)/i
);
if (rgbMatch) {
return {
r: Number.parseInt(rgbMatch[1], 10),
g: Number.parseInt(rgbMatch[2], 10),
b: Number.parseInt(rgbMatch[3], 10),
a: rgbMatch[4] === undefined ? 1 : Number.parseFloat(rgbMatch[4]),
};
}
const hexMatch = value.match(/^#([\da-f]{3,8})$/i);
if (!hexMatch) {
return null;
}
const hexValue = hexMatch[1];
if (hexValue.length === 3 || hexValue.length === 4) {
const [r, g, b, a = "f"] = hexValue.split("");
return {
r: Number.parseInt(r + r, 16),
g: Number.parseInt(g + g, 16),
b: Number.parseInt(b + b, 16),
a: Number.parseInt(a + a, 16) / 255,
};
}
if (hexValue.length === 6 || hexValue.length === 8) {
return {
r: Number.parseInt(hexValue.slice(0, 2), 16),
g: Number.parseInt(hexValue.slice(2, 4), 16),
b: Number.parseInt(hexValue.slice(4, 6), 16),
a:
hexValue.length === 8
? Number.parseInt(hexValue.slice(6, 8), 16) / 255
: 1,
};
}
return null;
}
function mix(base, tint, amount) {
const ratio = clamp(amount, 0, 1);
return {
r: Math.round(base.r + (tint.r - base.r) * ratio),
g: Math.round(base.g + (tint.g - base.g) * ratio),
b: Math.round(base.b + (tint.b - base.b) * ratio),
a: base.a + (tint.a - base.a) * ratio,
};
}
function toColor(color, alphaOverride) {
const alpha = alphaOverride ?? color.a ?? 1;
return `rgba(${color.r}, ${color.g}, ${color.b}, ${clamp(alpha, 0, 1)})`;
}
function clamp(value, min, max) {
return Math.min(Math.max(value, min), max);
}
function findUserAnchor(container) {
const selector = [
`button[label="${USER_BUTTON_LABEL}"]`,
`[role="button"][label="${USER_BUTTON_LABEL}"]`,
`oj-button[label="${USER_BUTTON_LABEL}"]`,
`button[aria-label="${USER_BUTTON_LABEL}"]`,
`[role="button"][aria-label="${USER_BUTTON_LABEL}"]`,
`[label="${USER_BUTTON_LABEL}"]`,
`[aria-label="${USER_BUTTON_LABEL}"]`,
].join(", ");
const directMatch = container.querySelector(selector);
if (directMatch && isVisible(directMatch)) {
return directMatch;
}
const fallbackMatch = Array.from(container.querySelectorAll("*")).find(
(element) => {
if (!isVisible(element) || element.closest(`#${IDS.mount}`)) {
return false;
}
return (
element.getAttribute("label") === USER_BUTTON_LABEL ||
element.getAttribute("aria-label") === USER_BUTTON_LABEL
);
}
);
return fallbackMatch || null;
}
function findTextElement(container, expectedText) {
const normalizedExpected = normalizeText(expectedText);
const elements = Array.from(container.querySelectorAll("*"));
return (
elements.find((element) => {
const text = normalizeText(element.textContent || "");
return text && text.includes(normalizedExpected);
}) || null
);
}
function resolveInsertionTarget(container, anchor, mount) {
const directAnchor = anchor ? getDirectChild(anchor, container) : null;
const fallbackTarget = findFirstInsertionTarget(container, mount);
const candidate = directAnchor || fallbackTarget;
if (!candidate) {
return null;
}
if (candidate === mount) {
return findNextValidSibling(container, mount);
}
return candidate.parentElement === container ? candidate : null;
}
function getDirectChild(element, container) {
let current = element;
while (current && current.parentElement !== container) {
current = current.parentElement;
}
return current && current.parentElement === container ? current : null;
}
function findFirstInsertionTarget(container, mount) {
return Array.from(container.children).find((child) => child !== mount) || null;
}
function findNextValidSibling(container, mount) {
let sibling = mount.nextElementSibling;
while (sibling) {
if (sibling.parentElement === container) {
return sibling;
}
sibling = sibling.nextElementSibling;
}
return null;
}
function findVisibleChildColor(container) {
const visibleChild = Array.from(container.querySelectorAll("*")).find(
(element) => isVisible(element) && !element.closest(`#${IDS.mount}`)
);
return visibleChild
? parseColor(window.getComputedStyle(visibleChild).color)
: null;
}
function extractArray(value, candidateKeys = []) {
if (Array.isArray(value)) {
return value;
}
if (!value || typeof value !== "object") {
return [];
}
for (const key of candidateKeys) {
if (Array.isArray(value[key])) {
return value[key];
}
}
for (const nestedValue of Object.values(value)) {
if (Array.isArray(nestedValue)) {
return nestedValue;
}
if (nestedValue && typeof nestedValue === "object") {
const nestedArray = extractArray(nestedValue, candidateKeys);
if (nestedArray.length > 0) {
return nestedArray;
}
}
}
return [];
}
async function mapWithConcurrency(items, concurrency, mapper) {
const results = new Array(items.length);
let nextIndex = 0;
async function worker() {
while (nextIndex < items.length) {
const currentIndex = nextIndex;
nextIndex += 1;
results[currentIndex] = await mapper(items[currentIndex], currentIndex);
}
}
const workerCount = Math.min(concurrency, items.length || 1);
await Promise.all(Array.from({ length: workerCount }, () => worker()));
return results;
}
function getStoredTheme() {
try {
const storedTheme = window.localStorage.getItem(STORAGE_KEYS.theme);
return storedTheme === "dark" ? "dark" : "light";
} catch {
return "light";
}
}
function setStoredTheme(theme) {
try {
window.localStorage.setItem(STORAGE_KEYS.theme, theme);
} catch {
// Ignore storage issues and keep the in-memory theme.
}
}
function getStoredSidebarCollapsed() {
try {
return window.localStorage.getItem(STORAGE_KEYS.sidebarCollapsed) === "true";
} catch {
return false;
}
}
function setStoredSidebarCollapsed(value) {
try {
window.localStorage.setItem(STORAGE_KEYS.sidebarCollapsed, String(Boolean(value)));
} catch {
// Ignore storage issues and keep the in-memory state.
}
}
function getStoredCalendarView() {
try {
return window.localStorage.getItem(STORAGE_KEYS.calendarView) === "week"
? "week"
: "month";
} catch {
return "month";
}
}
function setStoredCalendarView(value) {
try {
window.localStorage.setItem(
STORAGE_KEYS.calendarView,
value === "week" ? "week" : "month"
);
} catch {
// Ignore storage issues and keep the in-memory calendar view.
}
}
function normalizeText(value) {
return String(value || "").replace(/\s+/g, " ").trim();
}
function cleanString(value) {
return normalizeText(value);
}
function firstNonEmptyString(values) {
for (const value of values) {
const normalized = cleanString(value);
if (normalized) {
return normalized;
}
}
return "";
}
function extractAuthHeaders(tokenPayload) {
if (!tokenPayload) {
return {};
}
if (typeof tokenPayload === "string") {
return {
authorization: `Bearer ${tokenPayload}`,
};
}
const tokenSources = [
tokenPayload,
tokenPayload?.token,
tokenPayload?.tokens,
tokenPayload?.oauth,
tokenPayload?.oauthToken,
tokenPayload?.authentication,
tokenPayload?.auth,
].filter(Boolean);
const headers = {};
const explicitHeaderSources = [
tokenPayload?.headers,
tokenPayload?.requestHeaders,
tokenPayload?.authHeaders,
].filter((value) => value && typeof value === "object");
for (const explicitHeaderSource of explicitHeaderSources) {
for (const [key, value] of Object.entries(explicitHeaderSource)) {
const normalizedKey = cleanString(key).toLowerCase();
const normalizedValue = cleanString(value);
if (!normalizedKey || !normalizedValue) {
continue;
}
headers[normalizedKey] = normalizedValue;
}
}
for (const tokenSource of tokenSources) {
const tokenType =
firstNonEmptyString([
tokenSource?.tokenType,
tokenSource?.type,
tokenSource?.token_type,
]) || "Bearer";
const accessToken = firstNonEmptyString([
tokenSource?.accessToken,
tokenSource?.access_token,
tokenSource?.token,
tokenSource?.jwt,
]);
const idToken = firstNonEmptyString([
tokenSource?.idToken,
tokenSource?.id_token,
]);
if (accessToken && !headers.authorization) {
headers.authorization = `${tokenType} ${accessToken}`;
}
if (idToken && !headers["x-id-token"]) {
headers["x-id-token"] = idToken;
}
}
return headers;
}
function normalizeString(value) {
return normalizeText(value).toUpperCase();
}
function toBridgeRequestUrl(url) {
const normalized = resolveRequestUrl(url);
if (!normalized) {
return normalized;
}
try {
const parsedUrl = new URL(normalized, window.location.origin);
if (parsedUrl.origin === window.location.origin) {
return `${parsedUrl.pathname}${parsedUrl.search}${parsedUrl.hash}`;
}
return parsedUrl.toString();
} catch {
return normalized;
}
}
function resolveRequestUrl(url) {
const normalized = cleanString(url);
if (!normalized) {
return "";
}
try {
const baseOrigin =
isHcmWelcomePage() && normalized.startsWith("/")
? TARGET_ORIGIN
: window.location.origin;
return new URL(normalized, baseOrigin).toString();
} catch {
return normalized;
}
}
function toNumber(value) {
if (typeof value === "number" && Number.isFinite(value)) {
return value;
}
const numeric = Number.parseFloat(String(value ?? "").replace(/,/g, ""));
return Number.isFinite(numeric) ? numeric : 0;
}
function toIsoDateString(value) {
const normalized = cleanString(value);
if (!normalized) {
return "";
}
const date = new Date(`${normalized}T12:00:00.000Z`);
return Number.isNaN(date.getTime()) ? normalized : date.toISOString();
}
function isVisible(element) {
const style = window.getComputedStyle(element);
const rect = element.getBoundingClientRect();
return (
style.display !== "none" &&
style.visibility !== "hidden" &&
Number.parseFloat(style.opacity || "1") > 0 &&
rect.width > 0 &&
rect.height > 0
);
}
function formatWholeNumber(value) {
return new Intl.NumberFormat("en-US", {
maximumFractionDigits: 0,
}).format(value);
}
function formatCurrency(value) {
return new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
maximumFractionDigits: 0,
}).format(value || 0);
}
function formatHours(value) {
return new Intl.NumberFormat("pt-BR", {
maximumFractionDigits: 2,
}).format(toNumber(value));
}
function formatWeekNumber(value) {
return String(toNumber(value)).padStart(2, "0");
}
function formatOperationalWeekLabel(weekInfo) {
if (!weekInfo) {
return "";
}
return `FY${formatWeekNumber(weekInfo.fiscalYear % 100)}-Q${
weekInfo.quarter
}-W${formatWeekNumber(weekInfo.weekNumber)}`;
}
function formatPercent(value) {
return new Intl.NumberFormat("en-US", {
maximumFractionDigits: 1,
}).format(value || 0) + "%";
}
function formatProgressPercent(value) {
return new Intl.NumberFormat("en-US", {
maximumFractionDigits: 0,
}).format(value || 0) + "%";
}
function formatRelativeMoment(value) {
if (!value) {
return "-";
}
const target = new Date(value);
if (Number.isNaN(target.getTime())) {
return String(value);
}
let remaining = Math.max(Date.now() - target.getTime(), 0);
const day = 24 * 60 * 60 * 1000;
const hour = 60 * 60 * 1000;
const minute = 60 * 1000;
const second = 1000;
const days = Math.floor(remaining / day);
remaining -= days * day;
const hours = Math.floor(remaining / hour);
remaining -= hours * hour;
const minutes = Math.floor(remaining / minute);
remaining -= minutes * minute;
const seconds = Math.floor(remaining / second);
return `${days}d ${hours}h ${minutes}m ${seconds}s`;
}
function formatDate(value) {
if (!value) {
return "";
}
const date = parseDatePreservingDateOnly(value);
if (Number.isNaN(date.getTime())) {
return String(value);
}
return new Intl.DateTimeFormat("en-US", {
dateStyle: "medium",
}).format(date);
}
function getDateInputValue(value) {
const date = parseDatePreservingDateOnly(value);
if (Number.isNaN(date.getTime())) {
return "";
}
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, "0");
const day = String(date.getDate()).padStart(2, "0");
return `${year}-${month}-${day}`;
}
function formatDateForTimeEntryRequest(value) {
const date = parseDatePreservingDateOnly(value);
if (Number.isNaN(date.getTime())) {
return "";
}
const day = String(date.getDate()).padStart(2, "0");
const month = new Intl.DateTimeFormat("en-US", { month: "short" })
.format(date)
.toUpperCase();
const year = date.getFullYear();
return `${day}-${month}-${year}`;
}
function parseTimeEntryDate(value) {
const text = cleanString(value);
const match = text.match(/^(\d{1,2})-([A-Z]{3})-(\d{4})$/i);
if (!match) {
return parseDatePreservingDateOnly(text);
}
const monthIndex = [
"JAN",
"FEB",
"MAR",
"APR",
"MAY",
"JUN",
"JUL",
"AUG",
"SEP",
"OCT",
"NOV",
"DEC",
].indexOf(match[2].toUpperCase());
if (monthIndex < 0) {
return new Date(Number.NaN);
}
return new Date(Number(match[3]), monthIndex, Number(match[1]));
}
function formatDateTime(value) {
if (!value) {
return "";
}
const date = new Date(value);
if (Number.isNaN(date.getTime())) {
return String(value);
}
return new Intl.DateTimeFormat("en-US", {
dateStyle: "medium",
timeStyle: "short",
}).format(date);
}
function getLocalDateKey(value) {
if (!value) {
return "";
}
const date = parseDatePreservingDateOnly(value);
if (Number.isNaN(date.getTime())) {
return "";
}
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, "0");
const day = String(date.getDate()).padStart(2, "0");
return `${year}-${month}-${day}`;
}
function getOperationalWeekInfo(value) {
const date = parseDatePreservingDateOnly(value);
if (Number.isNaN(date.getTime())) {
return null;
}
const operationalYearStart = new Date(date.getFullYear(), 5, 1);
if (date < operationalYearStart) {
operationalYearStart.setFullYear(operationalYearStart.getFullYear() - 1);
}
const normalizedDate = new Date(
date.getFullYear(),
date.getMonth(),
date.getDate()
);
const normalizedStart = new Date(
operationalYearStart.getFullYear(),
operationalYearStart.getMonth(),
operationalYearStart.getDate()
);
const daysSinceStart = Math.floor(
(normalizedDate.getTime() - normalizedStart.getTime()) / 86400000
);
const monthsSinceStart =
(normalizedDate.getFullYear() - normalizedStart.getFullYear()) * 12 +
normalizedDate.getMonth() -
normalizedStart.getMonth();
return {
fiscalYear: operationalYearStart.getFullYear() + 1,
quarter: Math.floor(monthsSinceStart / 3) + 1,
weekNumber: Math.floor(daysSinceStart / 7) + 1,
};
}
function getCalendarRowWeekDate(rowStartDate) {
const weekDate = new Date(rowStartDate);
// The calendar renders rows from Sunday, but ISO week numbering starts on Monday.
if (weekDate.getDay() === 0) {
weekDate.setDate(weekDate.getDate() + 1);
}
return weekDate;
}
function normalizeTimeEntriesSummary(payload) {
if (Array.isArray(payload)) {
return payload;
}
const items = extractArray(payload, [
"items",
"data",
"results",
"content",
"entries",
"timeEntries",
"summary",
"summaries",
]);
if (items.length) {
return items;
}
if (payload && typeof payload === "object") {
return [payload];
}
return [];
}
function groupTimeEntriesByResourceName(items) {
const groups = new Map();
for (const item of items) {
const resourceName =
getTimeEntryResourceName(item) || "Sem resourceName";
const group = groups.get(resourceName) || {
resourceName,
items: [],
};
group.items.push(item);
groups.set(resourceName, group);
}
return Array.from(groups.values()).sort((left, right) => {
return left.resourceName.localeCompare(right.resourceName);
});
}
function getTimeEntryResourceName(item) {
const directValue = getFieldValue(item, [
"resourceName",
"ResourceName",
"resource_name",
"resource.name",
"Resource.Name",
"resource.displayName",
"Resource.DisplayName",
"resource.display_name",
"employeeName",
"EmployeeName",
"personName",
"PersonName",
]);
if (directValue) {
return cleanString(directValue);
}
const flattenedMatch = flattenDisplayRecord(item).find(([key, value]) => {
return key.toLowerCase().endsWith("resourcename") && cleanString(value);
});
return flattenedMatch ? cleanString(flattenedMatch[1]) : "";
}
function getTimeEntrySrNumber(item) {
return cleanString(
getTimeEntryField(item, [
"srNumber",
"SrNumber",
"SRNumber",
"serviceRequestNumber",
"ServiceRequestNumber",
"serviceRequest.srNumber",
"ServiceRequest.SrNumber",
])
);
}
function getTimeEntryActivity(item) {
return cleanString(
getTimeEntryField(item, [
"activityCode",
"ActivityCode",
"activity.code",
"Activity.Code",
])
);
}
function getTimeEntryTaskType(item) {
return cleanString(
getTimeEntryField(item, [
"TaskType",
"taskType",
"task_type",
"task.type",
"Task.Type",
])
);
}
function getTimeEntryTimeSpent(item) {
return toNumber(
getTimeEntryField(item, [
"timeSpent",
"TimeSpent",
"time_spent",
"time.spent",
"Time.Spent",
])
);
}
function getTimeEntryDateKey(item, weekDays = []) {
const rawDate = cleanString(
getTimeEntryField(item, [
"Date",
"date",
"dateValue",
"DateValue",
"timeEntryDate",
"TimeEntryDate",
"entryDate",
"EntryDate",
])
);
if (!rawDate) {
return weekDays.length === 1 ? getTimeManagementDayKey(weekDays[0]) : "";
}
const parsedDate = parseTimeEntryDate(rawDate);
if (!Number.isNaN(parsedDate.getTime())) {
return getLocalDateKey(parsedDate);
}
const matchingDay = (weekDays || []).find((day) => {
return normalizeString(day?.date) === normalizeString(rawDate);
});
return matchingDay ? getTimeManagementDayKey(matchingDay) : "";
}
function getTimeEntryField(item, keys) {
const directValue = getFieldValue(item, keys);
if (directValue !== undefined && directValue !== null && directValue !== "") {
return directValue;
}
const normalizedKeys = keys.map((key) => key.toLowerCase().replace(/[^a-z0-9]/g, ""));
const flattenedMatch = flattenDisplayRecord(item).find(([key, value]) => {
const normalizedKey = key.toLowerCase().replace(/[^a-z0-9]/g, "");
return normalizedKeys.includes(normalizedKey) && cleanString(value);
});
return flattenedMatch ? flattenedMatch[1] : "";
}
function flattenDisplayRecord(value, prefix = "", output = []) {
if (!value || typeof value !== "object" || Array.isArray(value)) {
if (prefix) {
output.push([prefix, value]);
}
return output;
}
for (const [key, nestedValue] of Object.entries(value)) {
const nextKey = prefix ? `${prefix}.${key}` : key;
if (
nestedValue &&
typeof nestedValue === "object" &&
!Array.isArray(nestedValue)
) {
flattenDisplayRecord(nestedValue, nextKey, output);
} else {
output.push([nextKey, nestedValue]);
}
}
return output;
}
function formatDisplayValue(value) {
if (value === null || value === undefined || value === "") {
return "-";
}
if (Array.isArray(value) || typeof value === "object") {
return JSON.stringify(value);
}
return String(value);
}
function parseDatePreservingDateOnly(value) {
if (value instanceof Date) {
return value;
}
if (typeof value === "string") {
const dateOnlyMatch = value.trim().match(/^(\d{4})-(\d{2})-(\d{2})$/);
if (dateOnlyMatch) {
return new Date(
Number(dateOnlyMatch[1]),
Number(dateOnlyMatch[2]) - 1,
Number(dateOnlyMatch[3])
);
}
}
return new Date(value);
}
function getErrorMessage(error) {
if (error instanceof Error) {
return error.message;
}
return "Unexpected error while fetching live data.";
}
function escapeHtml(value) {
return String(value ?? "")
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#39;");
}
function lockScroll() {
document.documentElement.classList.add(CLASSES.modalOpen);
document.body.classList.add(CLASSES.modalOpen);
}
function unlockScroll() {
document.documentElement.classList.remove(CLASSES.modalOpen);
document.body.classList.remove(CLASSES.modalOpen);
}
function removeModal() {
const overlay = document.getElementById(IDS.overlay);
window.clearTimeout(closeTimer);
restoreFocusTarget = null;
if (overlay) {
overlay.remove();
}
}
function handleGlobalKeydown(event) {
if (event.key !== "Escape") {
return;
}
if (appState.actionFormModal) {
appState.actionFormModal = null;
renderModal();
return;
}
if (appState.timeManagementModal) {
appState.timeManagementModal = null;
renderModal();
return;
}
if (appState.forecastUpdateConfirmModal) {
appState.forecastUpdateConfirmModal = null;
renderModal();
return;
}
if (appState.rampComparisonModal) {
appState.rampComparisonModal = null;
renderModal();
return;
}
if (appState.timeEntriesDrawer) {
appState.timeEntriesDrawer = null;
renderModal();
return;
}
if (appState.detailModal) {
closeDetailModal();
renderModal();
return;
}
closeModal();
}
function handleBridgeMessage(event) {
const message = event.data;
if (!message || message.source !== BRIDGE.source) {
return;
}
if (message.type === BRIDGE.workbenchAuthHeadersType) {
rememberWorkbenchAuthHeaders(message.headers);
return;
}
if (event.source !== window) {
return;
}
if (message.type === BRIDGE.readyType) {
return;
}
if (
message.type !== BRIDGE.resultType &&
message.type !== BRIDGE.headersResultType
) {
return;
}
const pending = pendingBridgeRequests.get(message.requestId);
if (!pending) {
return;
}
pendingBridgeRequests.delete(message.requestId);
window.clearTimeout(pending.timeout);
pending.resolve(
message.type === BRIDGE.headersResultType ? message.headers || {} : message
);
}
function rememberWorkbenchAuthHeaders(headers) {
if (!headers || typeof headers !== "object") {
return;
}
const nextHeaders = {
...(appState.sessionAuthHeaders || {}),
};
for (const [key, value] of Object.entries(headers)) {
const normalizedKey = cleanString(key).toLowerCase();
const normalizedValue = cleanString(value);
if (!normalizedKey || !normalizedValue) {
continue;
}
nextHeaders[normalizedKey] = normalizedValue;
}
appState.sessionAuthHeaders = nextHeaders;
}
function observePage() {
const observer = new MutationObserver(() => {
scheduleSync();
});
observer.observe(document.documentElement, {
childList: true,
subtree: true,
attributes: true,
attributeFilter: ["src"],
});
}
function observeHistory() {
const { pushState, replaceState } = window.history;
window.history.pushState = function pushStatePatched(...args) {
const result = pushState.apply(this, args);
scheduleSync();
return result;
};
window.history.replaceState = function replaceStatePatched(...args) {
const result = replaceState.apply(this, args);
scheduleSync();
return result;
};
window.addEventListener("popstate", scheduleSync);
window.addEventListener("hashchange", scheduleSync);
}
function observeWorklistPageLoadToken() {
if (!isHcmWelcomePage()) {
return;
}
const syncLoadedToken = () => {
syncWorklistIframeToken({ force: true });
};
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", syncLoadedToken, { once: true });
} else {
syncLoadedToken();
}
window.addEventListener("load", syncLoadedToken);
window.addEventListener("pageshow", syncLoadedToken);
}
function observeSpaFrameLifecycle() {
if (!isHcmWelcomePage()) {
return;
}
const syncFrame = () => {
ensureSpaFrame();
};
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", syncFrame, { once: true });
} else {
syncFrame();
}
window.addEventListener("load", syncFrame);
window.addEventListener("pageshow", syncFrame);
window.setInterval(syncFrame, 5000);
}
window.addEventListener("message", handleBridgeMessage);
document.addEventListener("keydown", handleGlobalKeydown);
void ensurePageBridge();
observePage();
observeHistory();
observeWorklistPageLoadToken();
observeSpaFrameLifecycle();
scheduleSync();
})();