(() => {
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",
timeManagementSelectedDate: "arch-panel-extension-time-management-selected-date",
};
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,
},
timeManagementActivityCache: new Map(),
pendingTimeManagementHighlightRowId: "",
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 = `
`;
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-save") {
void saveTimeManagementChanges();
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 = "";
scrollPendingTimeManagementHighlightIntoView(overlay);
});
}
function scrollPendingTimeManagementHighlightIntoView(overlay) {
const rowId = cleanString(appState.pendingTimeManagementHighlightRowId);
if (!rowId) {
return;
}
appState.pendingTimeManagementHighlightRowId = "";
const row = overlay.querySelector(
`.arch-panel-extension-time-management-entry-row[data-row-id="${cssEscape(rowId)}"]`
);
if (row instanceof HTMLElement) {
row.scrollIntoView({
behavior: "smooth",
block: "center",
inline: "nearest",
});
}
}
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 `
${renderMainContent()}
${renderDetailModal()}
${renderActionFormModal()}
${renderRampComparisonModal()}
${renderForecastUpdateConfirmModal()}
${renderTimeEntriesDrawer()}
${renderTimeManagementModal()}
`;
}
function renderMainContent() {
if (appState.status === "loading") {
return `
Loading data
Building the workload dataset
${escapeHtml(appState.loadingMessage)}
Overall progress
${formatProgressPercent(appState.loadingProgress || 0)}
`;
}
if (appState.status === "error") {
return `
Request failed
Unable to assemble the dashboard
${escapeHtml(appState.errorMessage)}
Retry
`;
}
if (!appState.dataset) {
return `
Ready
Start the live data extraction
No cached snapshot is available yet. Use Refresh data to fetch the current user, customers, workloads, actions and service requests.
Refresh data
`;
}
const { user, summary } = appState.dataset;
return `
${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")}
${summary.stageIndicators.map((indicator) => renderStageIndicator(indicator)).join("")}
${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"
)}
${renderSrHoursAndCalendarSection(summary)}
`;
}
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 `
${
avatar
? `
`
: `
${escapeHtml(initials)} `
}
`;
}
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 `
${escapeHtml(label)}
${escapeHtml(value)}
${escapeHtml(detail)}
`;
}
function renderStageIndicator(indicator) {
return `
${escapeHtml(indicator.label)}
${formatWholeNumber(indicator.count)}
Total (ACR): ${formatCurrency(indicator.totalAcr)}
`;
}
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 `
${escapeHtml(trueLabel)}: ${formatPercent(truePercent)}
${escapeHtml(falseLabel)}: ${formatPercent(falsePercent)}
${escapeHtml(trueLabel)}
${formatWholeNumber(trueCount)}
${formatPercent(truePercent)}
${escapeHtml(falseLabel)}
${formatWholeNumber(falseCount)}
${formatPercent(falsePercent)}
`;
}
function renderSrHoursAndCalendarSection(summary) {
const calendar = buildWorkloadStartCalendar(
summary.allWorkloads,
appState.calendarYear,
appState.calendarMonth,
appState.calendarView,
appState.calendarAnchorDateKey
);
return `
${renderSrHoursIndicatorCard(summary)}
${renderWorkloadStartCalendar(calendar)}
`;
}
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 `
SRs 0-4h: ${formatPercent(zeroToFourPercent)}
SRs >4h: ${formatPercent(aboveFourPercent)}
${renderSrHoursIndicator(
"SRs 0-4h",
formatPercent(zeroToFourPercent),
zeroToFourCount,
"0-4"
)}
${renderSrHoursIndicator(
"SRs >4h",
formatPercent(aboveFourPercent),
aboveFourCount,
"above-4"
)}
`;
}
function renderSrHoursIndicator(label, detail, count, value) {
return `
${escapeHtml(label)}
${formatWholeNumber(count)}
${escapeHtml(detail)}
`;
}
function renderWorkloadStartCalendar(calendar) {
return `
${["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]
.map((day) => `${day} `)
.join("")}
${calendar.days
.map((day) => renderCalendarDay(day, calendar.view))
.join("")}
`;
}
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 `
${day.dayNumber}
${
day.isWeekStart
? `${escapeHtml(
day.operationalWeekLabel
)} `
: ""
}
${
day.workloads.length
? `${escapeHtml(
calendarWorkloadLabel
)} ${formatWholeNumber(
day.workloads.length
)} `
: ""
}
`;
}
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 `
${day.dayNumber}
${
day.isWeekStart
? `${escapeHtml(
day.operationalWeekLabel
)} `
: ""
}
${
day.workloads.length
? day.workloads.map((workload) => renderCalendarWeekWorkload(workload)).join("")
: `No starts `
}
`;
}
function renderCalendarWeekWorkload(workload) {
const label = workload.description || workload.name || "Workload";
const customer = workload.customerName || "";
return `
${escapeHtml(label)}
${
customer
? `${escapeHtml(
customer
)} `
: ""
}
`;
}
function renderWorkloadTable(workloads) {
return `
Opportunity
Customer
Workload
ACR
Type
Tem SR?
Tem Action?
${workloads.map((workload) => renderWorkloadTableRow(workload)).join("")}
`;
}
function renderWorkloadTableRow(
workload,
includeConsumptionStartDate = false,
includeRampMonths = false
) {
const hasRampMismatch =
includeRampMonths && hasRampForecastMismatch(workload);
return `
${renderOpportunityLink(workload)}
${escapeHtml(workload.customerName || "")}
${renderWorkloadLink(workload, hasRampMismatch)}
${
includeConsumptionStartDate
? `${escapeHtml(formatDate(workload.consumptionStartDate) || "-")} `
: ""
}
${
includeRampMonths
? `${renderRampMonthsButton(workload)} `
: ""
}
${formatCurrency(workload.adjustedACR)}
${escapeHtml(workload.opportunityForecastTypeGroup)}
${renderBooleanBadge(workload.hasSR)}
${renderHasActionCell(workload)}
`;
}
function renderRampMonthsButton(workload) {
return `
${formatWholeNumber(workload.rampMonths || 0)}
`;
}
function renderBooleanBadge(value) {
const isTrue = Boolean(value);
const variantClass = isTrue ? "is-success" : "is-danger";
const label = isTrue ? "Sim" : "Não";
return `
${label}
`;
}
function renderHasActionCell(workload) {
if (workload.hasAction) {
return renderBooleanBadge(true);
}
return `
${renderBooleanBadge(false)}
➕
`;
}
function renderInsightRow(label, value, detail) {
return `
${escapeHtml(label)}
${escapeHtml(detail)}
${escapeHtml(value)}
`;
}
function renderDetailModal() {
if (!appState.detailModal) {
return "";
}
const isServiceRequestDetail = appState.detailModal.type === "serviceRequests";
const detailItems = isServiceRequestDetail
? appState.detailModal.items || []
: getSortedDetailItems();
return `
${
isServiceRequestDetail
? `${formatWholeNumber(detailItems.length)} SRs `
: `${formatWholeNumber(detailItems.length)} workloads
Total (ACR): ${formatCurrency(appState.detailModal.totalAcr)} `
}
${
isServiceRequestDetail
? renderServiceRequestDetailTable(detailItems)
: renderDetailTable(
detailItems,
appState.detailModal.scope === "calendar-day"
)
}
${
isServiceRequestDetail
? `
⏱️ Manage time
`
: ""
}
Fechar
`;
}
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 `
Create workload action
Criar action
Fechar
${renderOpportunityLink(workload)}
${escapeHtml(workload.customerName || "")}
${escapeHtml(workload.name || "Unnamed workload")}
${modalError ? `
${escapeHtml(modalError)}
` : ""}
`;
}
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 `
Ramp forecast comparison
${escapeHtml(title)}
Ramp months: ${formatWholeNumber(modal.workload.rampMonths || 0)}
Adjusted ACR: ${formatCurrency(modal.workload.adjustedACR)}
Consumption start: ${escapeHtml(
formatDate(modal.workload.consumptionStartDate) || "-"
)}
${renderRampComparisonPivotTable(rows)}
Atualizar pelo Calculated ramp
Fechar
`;
}
function renderRampComparisonPivotTable(rows) {
if (!rows.length) {
return `Nenhum forecast encontrado para comparar.
`;
}
return `
Period
${rows.map((row) => `${escapeHtml(row.periodLabel)} `).join("")}
Current adjusted
${rows
.map(
(row) =>
`${formatCurrency(row.currentValue)} `
)
.join("")}
Calculated ramp
${rows
.map(
(row) =>
`${formatCurrency(row.calculatedValue)} `
)
.join("")}
Delta
${rows
.map(
(row) =>
`${formatCurrency(row.delta)} `
)
.join("")}
`;
}
function renderForecastUpdateConfirmModal() {
const modal = appState.forecastUpdateConfirmModal;
if (!modal) {
return "";
}
return `
Confirmar atualização
Atualizar forecast pelo Calculated ramp?
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.
${
modal.errorMessage
? `
${escapeHtml(
modal.errorMessage
)}
`
: ""
}
Cancelar
${modal.isSubmitting ? "Atualizando..." : "Confirmar atualização"}
`;
}
function renderTimeEntriesDrawer() {
const drawer = appState.timeEntriesDrawer;
if (!drawer) {
return "";
}
const items = Array.isArray(drawer.items) ? drawer.items : [];
return `
Horas apontadas
SR ${escapeHtml(drawer.srNumber || "-")}
${renderTimeEntriesDrawerBody(drawer, items)}
`;
}
function renderTimeEntriesDrawerBody(drawer, items) {
if (drawer.status === "loading") {
return `
Carregando
Buscando dados
Consultando o resumo de time entries na origem COMCIP.
`;
}
if (drawer.status === "error") {
return `
Erro na requisição
${escapeHtml(drawer.errorMessage || "Não foi possível carregar os apontamentos.")}
Tentar novamente
`;
}
const groupedItems = groupTimeEntriesByResourceName(items);
return `
${formatWholeNumber(items.length)} registros
${formatWholeNumber(groupedItems.length)} resources
${
items.length
? groupedItems.map((group) => renderTimeEntryGroup(group)).join("")
: `Nenhum registro retornado para esta SR.
`
}
`;
}
function renderTimeEntryGroup(group) {
const totalTimeSpent = group.items.reduce((sum, item) => {
return sum + getTimeEntryTimeSpent(item);
}, 0);
return `
resourceName
${escapeHtml(group.resourceName)}
${formatHours(totalTimeSpent)} total
Activity
Task type
Time spent
${group.items.map((item) => renderTimeEntryRow(item)).join("")}
Total
${formatHours(totalTimeSpent)}
`;
}
function renderTimeEntryRow(item) {
return `
${escapeHtml(getTimeEntryActivity(item) || "-")}
${escapeHtml(getTimeEntryTaskType(item) || "-")}
${formatHours(getTimeEntryTimeSpent(item))}
`;
}
function renderDetailTable(items, includeConsumptionStartDate = false) {
return `
${renderSortButton("Opportunity", DETAIL_SORT_KEYS.opportunityNumber)}
${renderSortButton("Customer", DETAIL_SORT_KEYS.customerName)}
${renderSortButton("Workload", DETAIL_SORT_KEYS.workload)}
${includeConsumptionStartDate ? "consumptionStartDate " : ""}
${renderSortButton("Ramp months", DETAIL_SORT_KEYS.rampMonths, true)}
${renderSortButton("ACR", DETAIL_SORT_KEYS.adjustedACR, true)}
${renderSortButton("Type", DETAIL_SORT_KEYS.opportunityForecastTypeGroup)}
${renderSortButton("Tem SR?", DETAIL_SORT_KEYS.hasSR)}
${renderSortButton("Tem Action?", DETAIL_SORT_KEYS.hasAction)}
${items
.map((workload) =>
renderWorkloadTableRow(workload, includeConsumptionStartDate, true)
)
.join("")}
`;
}
function renderTimeManagementModal() {
const modal = appState.timeManagementModal;
if (!modal) {
return "";
}
const selectedDate = modal.selectedDate || getDateInputValue(new Date());
const selectedDateLabel = formatDateInputDisplay(selectedDate);
const canSaveTimeManagement = isTimeManagementSaveEnabled(modal);
const isSavingTimeManagement = Boolean(modal.isSaving);
return `
Horas apontadas
Manage time
${renderTimeManagementBody(modal)}
${
modal.saveErrorMessage
? `
${escapeHtml(modal.saveErrorMessage)}
`
: ""
}
Fechar
`;
}
function renderTimeManagementBody(modal) {
if (modal.status === "loading") {
return renderTimeManagementSkeleton();
}
if (modal.status === "error") {
return `
Erro na requisição
${escapeHtml(
modal.errorMessage || "Não foi possÃvel carregar o apontamento."
)}
`;
}
const rows = getTimeManagementRows(modal);
return `
${renderTimeManagementHeaderRow(modal.weekDays || [])}
${
rows.length
? rows.map((row, index) => renderTimeManagementEntryRow(modal, row, index)).join("")
: `
Nenhum apontamento de hora neste período
`
}
`;
}
function renderTimeManagementHeaderRow(weekDays) {
return `
`;
}
function renderTimeManagementDayHeader(day) {
return `
${escapeHtml(day.dayLabel || "-")}
${escapeHtml(day.monthLabel || day.date || "-")}
`;
}
function renderTimeManagementEntryRow(modal, row, index) {
const rowId = cleanString(row?.id) || `row-${index}`;
const isHighlighted =
cleanString(modal?.highlightRowId) &&
cleanString(modal?.highlightRowId) === rowId;
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 `
${renderSearchableCombobox({
field: "sr",
rowId,
selectedValue: selectedSr,
value: selectedSrLabel,
placeholder: "Search SR",
options: modal.serviceRequests || [],
valueKey: "srNumber",
labelKey: "label",
fallbackLabelKey: "srTitle",
disabled: isNonServiceRequest,
})}
${renderSearchableCombobox({
field: "activity",
rowId,
selectedValue: selectedActivityValue,
value: selectedActivityLabel,
placeholder: "Search activity",
options: activityOptions,
valueKey: "value",
labelKey: "label",
metaKey: "value",
disabled: isNonServiceRequest,
})}
${renderSearchableCombobox({
field: "taskType",
rowId,
selectedValue: selectedTaskTypeValue,
value: selectedTaskTypeLabel,
placeholder: "Search task type",
options: taskTypeOptions,
valueKey: "value",
labelKey: "label",
})}
${(modal.weekDays || [])
.map((day) =>
renderTimeManagementDayInput(day, rowId, row?.dayHours)
)
.join("")}
`;
}
function renderTimeManagementSkeleton() {
return `
${Array.from({ length: 3 }, () => `
`).join("")}
${Array.from({ length: 7 }, () => `
`).join("")}
`;
}
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 `
${escapeHtml(value || placeholder || "")}
`;
}
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 `
${date.getDate()}
`;
});
return `
‹
${escapeHtml(monthLabel)}
›
${["S", "M", "T", "W", "T", "F", "S"].map((day) => `${day} `).join("")}
${days.join("")}
`;
}
function renderTimeManagementHourIndicators(modal) {
if (modal?.status === "loading") {
return `
${Array.from({ length: 4 }, () => `
`).join("")}
`;
}
return `
${buildTimeManagementHourIndicators(modal)
.map(
(indicator) => `
${escapeHtml(indicator.label)}
${escapeHtml(formatHours(indicator.value))}
`
)
.join("")}
`;
}
function buildTimeManagementHourIndicators(modal) {
const totals = getTimeManagementRows(modal).reduce(
(accumulator, row) => {
const rowTotal = getTimeManagementRowTotalHours(row);
const category = getTimeManagementHourCategory(modal, row);
accumulator[category] += rowTotal;
accumulator.total += rowTotal;
return accumulator;
},
{
service: 0,
nonService: 0,
timeOff: 0,
total: 0,
}
);
return [
{ label: "Service", value: totals.service },
{ label: "Non Service", value: totals.nonService },
{ label: "Time Off", value: totals.timeOff },
{ label: "Total hours", value: totals.total },
];
}
function getTimeManagementRowTotalHours(row) {
return Object.values(row?.dayHours || {}).reduce(
(sum, value) => sum + formatTimeManagementHourPayloadValue(value),
0
);
}
function getTimeManagementHourCategory(modal, row) {
if (isTimeManagementTimeOffRow(modal, row)) {
return "timeOff";
}
return isNonServiceRequestSrNumber(row?.selectedSrNumber)
? "nonService"
: "service";
}
function isTimeManagementTimeOffRow(modal, row) {
const taskTypeValue = cleanString(row?.selectedTaskTypeValue);
const taskType = findTimeManagementTaskTypeByValue(modal, taskTypeValue);
const candidates = [
taskTypeValue,
taskType?.label,
taskType?.tags,
].map((value) => normalizeString(value));
return candidates.some(
(value) =>
value === "TIME_OFF" ||
value === "TIMEOFF" ||
value.includes("TIME_OFF") ||
value.includes("TIME OFF")
);
}
function findTimeManagementTaskTypeByValue(modal, value) {
const normalizedValue = normalizeString(value);
if (!normalizedValue) {
return null;
}
return (modal?.taskTypes || []).find(
(item) => normalizeString(item?.value) === normalizedValue
) || null;
}
function renderTimeManagementDayInput(
day,
rowId = "",
dayHours = {}
) {
const dateKey = getTimeManagementDayKey(day);
const value = cleanString(dayHours?.[dateKey]);
return `
`;
}
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(`${escapeHtml(emptyLabel)} `);
}
return options
.concat(
items.map((item) => {
const value = cleanString(item?.[valueKey]);
const label = cleanString(item?.[labelKey]) || value || emptyLabel;
return `
${escapeHtml(label)}
`;
})
)
.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 `
Customer
SR Number
Queue
Service
Pillar
Oppty value
Horas apontadas
Customer deadline
Oppty Status
${items
.map(
(serviceRequest) => `
${escapeHtml(serviceRequest.customerName || "-")}
${renderServiceRequestLink(serviceRequest)}
${escapeHtml(serviceRequest.queueName || "-")}
${escapeHtml(serviceRequest.serviceName || "-")}
${escapeHtml(serviceRequest.pillarLabel || "-")}
${formatCurrency(serviceRequest.opportunityValue)}
${renderReportedHoursLink(serviceRequest)}
${escapeHtml(formatDate(serviceRequest.customerDeadline) || "-")}
${escapeHtml(serviceRequest.opportunityStatusLabel || "-")}
`
)
.join("")}
`;
}
function renderSortButton(label, sortKey, isNumeric = false) {
const currentKey = appState.detailModal?.sortKey || "";
const currentDirection = appState.detailModal?.sortDirection || "asc";
const indicator =
currentKey === sortKey ? (currentDirection === "asc" ? " ↑" : " ↓") : "";
return `
${escapeHtml(label)}${indicator}
`;
}
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 `${escapeHtml(label)} `;
}
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 `${escapeHtml(srNumber)} `;
}
function renderReportedHoursLink(serviceRequest) {
const srNumber = cleanString(serviceRequest?.srNumber);
const label = formatHours(serviceRequest?.totalHoursWorked);
if (!srNumber) {
return escapeHtml(label);
}
return `
${escapeHtml(label)}
`;
}
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 `
${escapeHtml(label)}
${renderRampAlertButton(workload, showRampAlert)}
`;
}
function renderRampAlertButton(workload, showRampAlert) {
if (!showRampAlert || !workload.workloadId) {
return "";
}
return `
!
`;
}
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 (shouldNavigateSpaFrame(existingFrame, frameUrl)) {
navigateSpaFrame(existingFrame, frameUrl);
}
return waitForFrameLoad(existingFrame);
}
const frame = document.createElement("iframe");
frame.id = IDS.spaFrame;
frame.name = IDS.spaFrame;
frame.dataset.archPanelFrame = "spa-workbench";
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) {
const desiredParent = document.body || document.documentElement;
if (desiredParent && existingHost.parentElement !== desiredParent) {
desiredParent.appendChild(existingHost);
}
Object.assign(existingHost.style, {
position: "fixed",
width: "1440px",
height: "1000px",
overflow: "hidden",
opacity: "0",
pointerEvents: "none",
border: "0",
left: "-20000px",
top: "0",
zIndex: "0",
});
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: "0",
});
(document.body || document.documentElement).appendChild(host);
return host;
}
function shouldNavigateSpaFrame(frame, frameUrl) {
const currentSrc = cleanString(frame.getAttribute("src") || frame.src);
if (!currentSrc || currentSrc === "about:blank") {
return true;
}
try {
return new URL(currentSrc, window.location.href).toString() !== frameUrl;
} catch {
return currentSrc !== frameUrl;
}
}
function navigateSpaFrame(frame, frameUrl) {
frame.dataset.archPanelLoaded = "false";
frame.src = frameUrl;
}
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 = getStoredTimeManagementSelectedDate();
appState.timeManagementModal = createTimeManagementLoadingState(selectedDate);
renderModal();
await loadTimeManagementData(selectedDate);
}
async function loadTimeManagementData(selectedDate) {
const normalizedDate = cleanString(selectedDate) || getDateInputValue(new Date());
const requestId = `${Date.now()}-${++requestCounter}`;
setStoredTimeManagementSelectedDate(normalizedDate);
appState.timeManagementDatePicker = {
...appState.timeManagementDatePicker,
isOpen: false,
};
appState.timeManagementModal = {
...(appState.timeManagementModal || {}),
requestId,
selectedDate: normalizedDate,
status: "loading",
errorMessage: "",
};
renderModal();
try {
const resourcePartyId = getCurrentUserResourcePartyId();
const taskTypePromise = fetchTaskTypeRequest();
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),
taskTypePromise,
fetchTimeEntryWeek(timeEntryWeekUrl),
]);
const timeEntryRows = normalizeTimeManagementEntryRows(
timeEntryWeekPayload,
weekDays
);
const serviceRequests = mergeTimeManagementServiceRequests(
normalizeTimeManagementServiceRequests(srPayload),
timeEntryRows
);
const activitySrNumbers = getActivityRequestSrNumbers(srPayload, serviceRequests);
const activityRequest = await fetchCachedActivityRequest(activitySrNumbers);
const activityRequestUrl = activityRequest.url;
const activityPayload = activityRequest.payload;
const activityOptionsBySr = normalizeActivityRequest(
activityPayload,
serviceRequests
);
const taskTypes = normalizeTaskTypeRequest(taskTypePayload);
const rows = timeEntryRows;
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 || "",
highlightRowId: "",
deletedTimeEntryIds: [],
isSaving: false,
saveErrorMessage: "",
};
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: [],
highlightRowId: "",
deletedTimeEntryIds: [],
isSaving: false,
saveErrorMessage: "",
};
}
function getTimeManagementRows(modal) {
return modal.timeEntryRows || [];
}
function isTimeManagementSaveEnabled(modal) {
const pendingChanges = getTimeManagementPendingChanges(modal);
const changedRows = [
...pendingChanges.newRows,
...pendingChanges.updatedRows,
];
if (
!pendingChanges.newRows.length &&
!pendingChanges.updatedRows.length &&
!pendingChanges.deletedIds.length
) {
return false;
}
return changedRows.every((row) => isTimeManagementRowReadyToSave(row));
}
function isTimeManagementRowReadyToSave(row) {
const selectedSrNumber = cleanString(row?.selectedSrNumber);
if (!selectedSrNumber) {
return false;
}
if (!hasPositiveTimeManagementHours(row)) {
return false;
}
if (isNonServiceRequestSrNumber(selectedSrNumber)) {
return Boolean(cleanString(row?.selectedTaskTypeValue));
}
return Boolean(
cleanString(row?.selectedActivityValue) &&
cleanString(row?.selectedTaskTypeValue)
);
}
function hasPositiveTimeManagementHours(row) {
return Object.values(row?.dayHours || {}).some((value) => {
const normalizedValue = cleanString(value);
if (!normalizedValue) {
return false;
}
const numericValue = Number(normalizedValue.replace(",", "."));
return Number.isFinite(numericValue) && numericValue > 0;
});
}
function createBlankTimeManagementEntryRow(srNumber = "", source = "blank") {
return {
id: `row-${Date.now()}-${Math.random().toString(16).slice(2)}`,
timeEntryId: "",
selectedSrNumber: cleanString(srNumber),
selectedActivityValue: "",
selectedTaskTypeValue: "",
dayHours: {},
source,
originalSignature: "",
};
}
function addTimeManagementEntryRow(type) {
if (!appState.timeManagementModal) {
return;
}
const existingDuplicateRow = findExistingTimeManagementDuplicateRow(
appState.timeManagementModal,
type
);
if (existingDuplicateRow) {
highlightTimeManagementDuplicateRow(existingDuplicateRow.id);
return;
}
const srNumber = type === "non-service" ? "non-service-sr" : "";
const nextRows = [
...getTimeManagementRows(appState.timeManagementModal),
createBlankTimeManagementEntryRow(srNumber, "manual"),
];
appState.timeManagementModal = {
...appState.timeManagementModal,
timeEntryRows: nextRows,
highlightRowId: "",
saveErrorMessage: "",
};
renderModal();
}
function removeTimeManagementEntryRow(rowId) {
if (!appState.timeManagementModal) {
return;
}
const normalizedRowId = cleanString(rowId);
const rows = getTimeManagementRows(appState.timeManagementModal);
const removedRow = rows.find((row) => cleanString(row?.id) === normalizedRowId);
const removedTimeEntryId = cleanString(removedRow?.timeEntryId);
const remainingRows = rows.filter(
(row) => cleanString(row?.id) !== normalizedRowId
);
const nextRows = remainingRows;
const deletedTimeEntryIds = removedTimeEntryId
? Array.from(
new Set([
...(appState.timeManagementModal.deletedTimeEntryIds || []),
removedTimeEntryId,
])
)
: appState.timeManagementModal.deletedTimeEntryIds || [];
appState.timeManagementModal = {
...appState.timeManagementModal,
timeEntryRows: nextRows,
selectedSrNumber: nextRows[0]?.selectedSrNumber || "",
selectedActivityValue: nextRows[0]?.selectedActivityValue || "",
selectedTaskTypeValue: nextRows[0]?.selectedTaskTypeValue || "",
highlightRowId: "",
deletedTimeEntryIds,
saveErrorMessage: "",
};
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 duplicateRow = findDuplicateTimeManagementSrRow(
appState.timeManagementModal,
selectedSrNumber,
rowId
);
if (duplicateRow) {
highlightTimeManagementDuplicateRow(duplicateRow.id);
return;
}
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 || "",
highlightRowId: "",
saveErrorMessage: "",
};
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 duplicateRow = findDuplicateTimeManagementNonServiceTaskTypeRow(
appState.timeManagementModal,
value,
rowId
);
if (duplicateRow) {
highlightTimeManagementDuplicateRow(duplicateRow.id);
return;
}
}
const nextRows = updateTimeManagementRows(appState.timeManagementModal, rowId, {
...(targetRow || createBlankTimeManagementEntryRow()),
[fieldName]: value,
});
appState.timeManagementModal = {
...appState.timeManagementModal,
timeEntryRows: nextRows,
[fieldName]: value,
highlightRowId: "",
saveErrorMessage: "",
};
}
function findDuplicateTimeManagementSrRow(modal, srNumber, currentRowId = "") {
const normalizedSrNumber = normalizeString(srNumber);
const normalizedCurrentRowId = cleanString(currentRowId);
if (!normalizedSrNumber || isNonServiceRequestSrNumber(srNumber)) {
return null;
}
return getTimeManagementRows(modal).find((row) => {
return (
cleanString(row?.id) !== normalizedCurrentRowId &&
normalizeString(row?.selectedSrNumber) === normalizedSrNumber
);
}) || null;
}
function findExistingTimeManagementDuplicateRow(modal, type) {
const seen = new Map();
for (const row of getTimeManagementRows(modal)) {
const rowId = cleanString(row?.id);
let key = "";
if (type === "non-service") {
if (!isNonServiceRequestSrNumber(row?.selectedSrNumber)) {
continue;
}
key = normalizeString(row?.selectedTaskTypeValue);
} else {
if (isNonServiceRequestSrNumber(row?.selectedSrNumber)) {
continue;
}
key = normalizeString(row?.selectedSrNumber);
}
if (!key) {
continue;
}
if (seen.has(key)) {
return getTimeManagementRows(modal).find(
(candidate) => cleanString(candidate?.id) === seen.get(key)
) || row;
}
seen.set(key, rowId);
}
return null;
}
function findDuplicateTimeManagementNonServiceTaskTypeRow(
modal,
taskTypeValue,
currentRowId = ""
) {
const normalizedTaskTypeValue = normalizeString(taskTypeValue);
const normalizedCurrentRowId = cleanString(currentRowId);
if (!normalizedTaskTypeValue) {
return null;
}
const currentRow = getTimeManagementRowById(modal, currentRowId);
if (!isNonServiceRequestSrNumber(currentRow?.selectedSrNumber)) {
return null;
}
return getTimeManagementRows(modal).find((row) => {
return (
cleanString(row?.id) !== normalizedCurrentRowId &&
isNonServiceRequestSrNumber(row?.selectedSrNumber) &&
normalizeString(row?.selectedTaskTypeValue) === normalizedTaskTypeValue
);
}) || null;
}
function highlightTimeManagementDuplicateRow(rowId) {
const normalizedRowId = cleanString(rowId);
if (!appState.timeManagementModal || !normalizedRowId) {
return;
}
appState.pendingTimeManagementHighlightRowId = normalizedRowId;
appState.timeManagementModal = {
...appState.timeManagementModal,
highlightRowId: normalizedRowId,
};
renderModal();
}
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
),
highlightRowId: "",
saveErrorMessage: "",
};
renderModal();
}
async function saveTimeManagementChanges() {
const modal = appState.timeManagementModal;
if (!modal || modal.isSaving || !isTimeManagementSaveEnabled(modal)) {
return;
}
appState.timeManagementModal = {
...modal,
isSaving: true,
saveErrorMessage: "",
};
renderModal();
try {
const resourceId = getCurrentUserResourcePartyId();
const weekId = cleanString(modal.weekId);
if (!resourceId) {
throw new Error("resourceCurrentUser.resourcePartyId não está disponÃvel.");
}
if (!weekId) {
throw new Error("WeekId não encontrado para gravação do apontamento.");
}
const changes = getTimeManagementPendingChanges(modal);
const baseUrl = COMCIP_REQUEST.timeEntriesUrl;
const deletedIds = changes.deletedIds;
const updatedPayload = changes.updatedRows.map((row) =>
createTimeManagementSavePayload(row, modal.weekDays, weekId, resourceId, {
includeId: true,
})
);
const newPayload = changes.newRows.map((row) =>
createTimeManagementSavePayload(row, modal.weekDays, weekId, resourceId)
);
for (const timeEntryId of deletedIds) {
await mutateComcipPayload(
"DELETE",
`${baseUrl}/${encodeURIComponent(timeEntryId)}`,
null,
"timeEntryDelete"
);
}
if (updatedPayload.length) {
await mutateComcipPayload(
"PUT",
baseUrl,
updatedPayload,
"timeEntryUpdate"
);
}
if (newPayload.length) {
await mutateComcipPayload("POST", baseUrl, newPayload, "timeEntryCreate");
}
await loadTimeManagementData(modal.selectedDate || getDateInputValue(new Date()));
} catch (error) {
appState.timeManagementModal = {
...(appState.timeManagementModal || modal),
isSaving: false,
saveErrorMessage: getErrorMessage(error),
};
renderModal();
}
}
function getTimeManagementPendingChanges(modal) {
const rows = getTimeManagementRows(modal);
return {
newRows: rows.filter((row) => isNewTimeManagementRow(row)),
updatedRows: rows.filter((row) => isUpdatedTimeManagementRow(row)),
deletedIds: (modal?.deletedTimeEntryIds || [])
.map((id) => cleanString(id))
.filter(Boolean),
};
}
function isNewTimeManagementRow(row) {
return cleanString(row?.source) === "manual" && !cleanString(row?.timeEntryId);
}
function isUpdatedTimeManagementRow(row) {
const timeEntryId = cleanString(row?.timeEntryId);
const originalSignature = cleanString(row?.originalSignature);
return Boolean(
timeEntryId &&
originalSignature &&
originalSignature !== createTimeManagementRowSignature(row)
);
}
function createTimeManagementRowSignature(row) {
return JSON.stringify({
srNumber: cleanString(row?.selectedSrNumber),
activityCode: cleanString(row?.selectedActivityValue),
taskType: cleanString(row?.selectedTaskTypeValue),
dayHours: normalizeTimeManagementDayHours(row?.dayHours || {}),
});
}
function normalizeTimeManagementDayHours(dayHours) {
return Object.keys(dayHours || {})
.sort()
.reduce((normalized, key) => {
normalized[key] = formatTimeManagementHourPayloadValue(dayHours[key]);
return normalized;
}, {});
}
function createTimeManagementSavePayload(
row,
weekDays,
weekId,
resourceId,
options = {}
) {
const selectedSrNumber = cleanString(row?.selectedSrNumber);
const payload = {
srNumber: selectedSrNumber,
activityCode: isNonServiceRequestSrNumber(selectedSrNumber)
? ""
: cleanString(row?.selectedActivityValue),
taskType: cleanString(row?.selectedTaskTypeValue),
deliveryType: "",
saturdayTaskHours: 0,
sundayTaskHours: 0,
mondayTaskHours: 0,
tuesdayTaskHours: 0,
wednesdayTaskHours: 0,
thursdayTaskHours: 0,
fridayTaskHours: 0,
notes: "",
weekId: toNumber(weekId) || weekId,
resourceId: cleanString(resourceId),
tag: isNonServiceRequestSrNumber(selectedSrNumber)
? "NON_SERVICE"
: "SERVICE",
};
if (options.includeId) {
const id = cleanString(row?.timeEntryId);
payload.id = toNumber(id) || id;
}
for (const day of weekDays || []) {
const weekdayName = getTimeManagementWeekdayName(day);
const dateKey = getTimeManagementDayKey(day);
const payloadKey = `${weekdayName}TaskHours`;
if (payloadKey in payload) {
payload[payloadKey] = formatTimeManagementHourPayloadValue(
row?.dayHours?.[dateKey]
);
}
}
return payload;
}
function formatTimeManagementHourPayloadValue(value) {
const normalizedValue = cleanString(value);
if (!normalizedValue) {
return 0;
}
const numericValue = Number(normalizedValue.replace(",", "."));
return Number.isFinite(numericValue) ? numericValue : 0;
}
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");
}
async function fetchCachedActivityRequest(srNumbers) {
const cacheKey = getActivityRequestCacheKey(srNumbers);
if (!cacheKey) {
return {
url: "",
payload: null,
};
}
const cachedRequest = appState.timeManagementActivityCache.get(cacheKey);
if (cachedRequest?.payload) {
return cachedRequest;
}
if (cachedRequest?.promise) {
return cachedRequest.promise;
}
const requestPromise = fetchActivityRequestWithFallback(srNumbers).then((result) => {
appState.timeManagementActivityCache.set(cacheKey, {
url: result.url,
payload: result.payload,
});
return result;
}).catch((error) => {
appState.timeManagementActivityCache.delete(cacheKey);
throw error;
});
appState.timeManagementActivityCache.set(cacheKey, {
promise: requestPromise,
});
return requestPromise;
}
async function fetchActivityRequestWithFallback(srNumbers) {
let activityRequestUrl = buildActivityRequestUrl(srNumbers);
let activityPayload = await fetchComcipPayload(
activityRequestUrl,
"activityRequest"
);
if (activityRequestUrl && !hasActivityRequestItems(activityPayload)) {
activityRequestUrl = buildActivityRequestUrl(
ACTIVITY_REQUEST_REFERENCE_SR_NUMBERS
);
activityPayload = await fetchComcipPayload(
activityRequestUrl,
"activityRequest"
);
}
return {
url: activityRequestUrl,
payload: activityPayload,
};
}
function getActivityRequestCacheKey(srNumbers) {
const normalizedSrNumbers = (srNumbers || [])
.map((srNumber) => cleanString(srNumber).toUpperCase())
.filter((srNumber) => /^SR\d+$/i.test(srNumber))
.sort();
return normalizedSrNumbers.join("|");
}
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;
}
async function mutateComcipPayload(method, url, payload, requestName) {
const response = await sendComcipRequestMessage({
method,
url,
payload,
timeoutMessage: `Timed out while executing ${requestName}.`,
});
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 timeEntryId = getTimeEntryId(item);
const srNumber = getTimeEntrySrNumber(item) || "non-service-sr";
const activityValue = getTimeEntryActivity(item);
const taskTypeValue = getTimeEntryTaskType(item);
return {
id: `entry-${index + 1}`,
timeEntryId,
selectedSrNumber: srNumber,
selectedActivityValue: activityValue,
selectedTaskTypeValue: taskTypeValue,
dayHours: getTimeEntryWeekDayHours(item, weekDays),
source: "timeEntryWeek",
originalSignature: "",
};
}
);
}
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}`,
timeEntryId: cleanString(row?.timeEntryId),
selectedSrNumber: srNumber,
selectedActivityValue:
findOptionValue(activityOptions, row?.selectedActivityValue) ||
cleanString(row?.selectedActivityValue) ||
"",
selectedTaskTypeValue:
findOptionValue(taskTypeOptions, row?.selectedTaskTypeValue) ||
cleanString(row?.selectedTaskTypeValue) ||
"",
dayHours: row?.dayHours || {},
originalSignature:
cleanString(row?.originalSignature) ||
(cleanString(row?.source) === "timeEntryWeek"
? createTimeManagementRowSignature({
...row,
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 `
Arch Panel Dashboard
`;
}
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-utility-button.is-outline {
border-color: #0b5cab;
background: transparent;
color: #0b5cab;
}
.arch-panel-extension-utility-button.is-outline:hover:not(:disabled) {
background: rgba(11, 92, 171, 0.08);
}
.arch-panel-extension-utility-button.is-outline:disabled {
border-color: var(--wb-border);
background: transparent;
color: var(--wb-text-muted);
cursor: not-allowed;
opacity: 0.62;
}
.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-time-management-toolbar {
display: flex;
align-items: end;
gap: 14px;
flex-wrap: wrap;
}
.arch-panel-extension-time-management-hour-indicators {
display: grid;
grid-template-columns: repeat(4, minmax(92px, 1fr));
gap: 8px;
flex: 1 1 520px;
}
.arch-panel-extension-time-management-hour-indicator {
display: grid;
gap: 3px;
min-height: 48px;
align-content: center;
border: 1px solid var(--wb-border);
border-radius: 8px;
background: var(--wb-panel);
padding: 7px 10px;
}
.arch-panel-extension-time-management-hour-indicator span {
color: var(--wb-text-muted);
font-size: 10px;
font-weight: 800;
letter-spacing: 0.06em;
text-transform: uppercase;
}
.arch-panel-extension-time-management-hour-indicator strong {
color: var(--wb-text);
font-size: 16px;
font-weight: 800;
line-height: 1.1;
}
.arch-panel-extension-time-management-hour-indicator .arch-panel-extension-skeleton-text {
width: 72%;
}
.arch-panel-extension-time-management-hour-indicator .arch-panel-extension-skeleton-title {
width: 42%;
height: 16px;
}
.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: 4px 0;
scrollbar-gutter: stable;
}
.arch-panel-extension-time-management-rows:focus-within {
overflow: visible;
}
.arch-panel-extension-time-management-empty {
grid-column: 1 / -1;
text-align: center;
}
.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-duplicate-highlight {
border-radius: 10px;
animation: arch-panel-extension-duplicate-pulse 1100ms ease-in-out 2;
box-shadow:
inset 0 0 0 3px rgba(217, 79, 43, 0.9),
0 0 0 2px rgba(217, 79, 43, 0.12);
padding: 4px;
margin: -4px 0;
}
.arch-panel-extension-time-management-entry-row.is-compact .arch-panel-extension-form-field {
gap: 0;
}
@keyframes arch-panel-extension-duplicate-pulse {
0% {
box-shadow:
inset 0 0 0 2px rgba(217, 79, 43, 0.35),
0 0 0 0 rgba(217, 79, 43, 0.22);
}
45% {
box-shadow:
inset 0 0 0 3px rgba(217, 79, 43, 0.95),
0 0 0 6px rgba(217, 79, 43, 0.14);
}
100% {
box-shadow:
inset 0 0 0 3px rgba(217, 79, 43, 0.72),
0 0 0 0 rgba(217, 79, 43, 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-time-management-save-error {
margin: 0 auto 0 0;
color: var(--wb-warning);
font-size: 12px;
font-weight: 700;
}
.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 getStoredTimeManagementSelectedDate() {
try {
const storedDate = window.localStorage.getItem(
STORAGE_KEYS.timeManagementSelectedDate
);
return isValidDateInputValue(storedDate)
? storedDate
: getDateInputValue(new Date());
} catch {
return getDateInputValue(new Date());
}
}
function setStoredTimeManagementSelectedDate(value) {
if (!isValidDateInputValue(value)) {
return;
}
try {
window.localStorage.setItem(
STORAGE_KEYS.timeManagementSelectedDate,
value
);
} catch {
// Ignore storage issues and keep the in-memory selected date.
}
}
function isValidDateInputValue(value) {
if (!/^\d{4}-\d{2}-\d{2}$/.test(String(value || ""))) {
return false;
}
const parsedDate = parseDatePreservingDateOnly(value);
return !Number.isNaN(parsedDate.getTime());
}
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 formatDateInputDisplay(value) {
const date = parseDatePreservingDateOnly(value);
if (Number.isNaN(date.getTime())) {
return cleanString(value);
}
const day = String(date.getDate()).padStart(2, "0");
const month = String(date.getMonth() + 1).padStart(2, "0");
const year = date.getFullYear();
return `${day}/${month}/${year}`;
}
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 getTimeEntryId(item) {
return cleanString(
getTimeEntryField(item, [
"id",
"Id",
"ID",
"timeEntryId",
"TimeEntryId",
"timeEntryID",
"TimeEntryID",
])
);
}
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, "&")
.replace(//g, ">")
.replace(/"/g, """)
.replace(/'/g, "'");
}
function cssEscape(value) {
if (window.CSS?.escape) {
return window.CSS.escape(String(value || ""));
}
return String(value || "").replace(/["\\]/g, "\\$&");
}
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();
})();