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

7790 lines
233 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
(() => {
const TARGET_ORIGIN = "https://spa.oracle.com";
const TARGET_PATHNAME =
"/oalcrm/web/api/g2m-consumer-application/ui/index.html";
const TARGET_PARAM = "ojr";
const TARGET_PARAM_VALUE = "workload_workbench";
const 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",
};
const CLASSES = {
modalOpen: "arch-panel-extension-modal-open",
overlayOpen: "is-open",
};
const STORAGE_KEYS = {
theme: "arch-panel-extension-theme",
sidebarCollapsed: "arch-panel-extension-sidebar-collapsed",
calendarView: "arch-panel-extension-calendar-view",
};
const CACHE = {
dbName: "arch-panel-extension-db",
dbVersion: 1,
storeName: "datasets",
datasetKey: "workload-dashboard",
};
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",
readyType: "ARCH_PANEL_BRIDGE_READY",
scriptId: "arch-panel-extension-page-bridge",
};
const API_ENDPOINTS = {
currentUser:
"/oalcrm/web/api/identity-management/users/current",
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",
appVersion: "version_1754044416761",
};
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,
pendingFocusSelector: "",
};
let syncFrame = 0;
let restoreFocusTarget = null;
let closeTimer = 0;
let requestCounter = 0;
let bridgeReadyPromise = null;
let cacheHydrationPromise = null;
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 scheduleSync() {
if (syncFrame) {
return;
}
syncFrame = window.requestAnimationFrame(() => {
syncFrame = 0;
syncButton();
});
}
function syncButton() {
const mount = document.getElementById(IDS.mount);
if (!isTargetPage()) {
mount?.remove();
removeModal();
unlockScroll();
return;
}
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 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);
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.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");
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 === "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 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 &&
requestedFocusSelector
) {
const focusTarget = overlay.querySelector(requestedFocusSelector);
if (focusTarget instanceof HTMLElement) {
focusTarget.focus({ preventScroll: true });
}
}
appState.pendingFocusSelector = "";
});
}
function captureModalScrollState(overlay) {
const mainShell = overlay.querySelector(".arch-panel-extension-main-shell");
const shellBody = overlay.querySelector(".arch-panel-extension-shell-body");
const dialog = overlay.querySelector(".arch-panel-extension-dialog");
const detailTable = overlay.querySelector(
".arch-panel-extension-table-wrap.is-detail"
);
return {
windowX: window.scrollX,
windowY: window.scrollY,
documentTop: document.documentElement.scrollTop,
bodyTop: document.body.scrollTop,
mainShellTop: mainShell ? mainShell.scrollTop : 0,
shellBodyTop: shellBody ? shellBody.scrollTop : 0,
dialogTop: dialog ? dialog.scrollTop : 0,
detailTableTop: detailTable ? detailTable.scrollTop : 0,
detailTableLeft: detailTable ? detailTable.scrollLeft : 0,
};
}
function restoreModalScrollState(overlay, scrollState) {
const restore = () => {
const mainShell = overlay.querySelector(".arch-panel-extension-main-shell");
const shellBody = overlay.querySelector(".arch-panel-extension-shell-body");
const dialog = overlay.querySelector(".arch-panel-extension-dialog");
const detailTable = overlay.querySelector(
".arch-panel-extension-table-wrap.is-detail"
);
if (mainShell) {
mainShell.scrollTop = scrollState.mainShellTop;
}
if (shellBody) {
shellBody.scrollTop = scrollState.shellBodyTop;
}
if (dialog) {
dialog.scrollTop = scrollState.dialogTop;
}
if (detailTable) {
detailTable.scrollTop = scrollState.detailTableTop;
detailTable.scrollLeft = scrollState.detailTableLeft;
}
document.documentElement.scrollTop = scrollState.documentTop;
document.body.scrollTop = scrollState.bodyTop;
window.scrollTo(scrollState.windowX, scrollState.windowY);
};
window.requestAnimationFrame(() => {
restore();
window.requestAnimationFrame(restore);
});
}
function getRestorableFocusSelector(element) {
if (!(element instanceof HTMLElement)) {
return "";
}
const action = element.getAttribute("data-action");
if (
action === "calendar-prev" ||
action === "calendar-next" ||
action === "calendar-today"
) {
return `[data-action="${action}"]`;
}
if (action === "calendar-view") {
const calendarView = element.getAttribute("data-calendar-view");
return calendarView
? `[data-action="calendar-view"][data-calendar-view="${calendarView}"]`
: `[data-action="${action}"]`;
}
const detailScope = element.getAttribute("data-detail-scope");
const detailValue = element.getAttribute("data-detail-value");
if (detailScope === "calendar-day" && detailValue) {
return getCalendarDayFocusSelector(detailValue);
}
return "";
}
function getCalendarDayFocusSelector(dateKey) {
return `[data-action="open-detail"][data-detail-scope="calendar-day"][data-detail-value="${dateKey}"]`;
}
function createModalMarkup() {
return `
<div
id="${IDS.dialog}"
class="arch-panel-extension-dialog"
role="dialog"
aria-modal="true"
aria-labelledby="arch-panel-extension-title"
tabindex="-1"
>
<header class="arch-panel-extension-shell-header">
<div class="arch-panel-extension-brand">
<span class="arch-panel-extension-brand-mark">Arch</span>
<div>
<p class="arch-panel-extension-brand-label">Enterprise workload intelligence</p>
<h1 id="arch-panel-extension-title" class="arch-panel-extension-title">
Arch panel
</h1>
</div>
</div>
<div class="arch-panel-extension-shell-actions">
<span class="arch-panel-extension-shell-note">
${getHeaderSummary()}
</span>
<button
type="button"
class="arch-panel-extension-theme-toggle"
data-action="toggle-sidebar"
aria-label="Toggle sidebar"
>
${appState.sidebarCollapsed ? "Show sidebar" : "Hide sidebar"}
</button>
<button
type="button"
class="arch-panel-extension-theme-toggle"
data-action="toggle-theme"
aria-label="Toggle dashboard theme"
>
${appState.theme === "dark" ? "Light mode" : "Dark mode"}
</button>
<button
type="button"
class="arch-panel-extension-close"
data-action="close"
aria-label="Close Arch panel"
>
Close
</button>
</div>
</header>
<div class="arch-panel-extension-shell-body${appState.sidebarCollapsed ? " is-sidebar-collapsed" : ""}">
${renderMainContent()}
${renderDetailModal()}
${renderActionFormModal()}
${renderRampComparisonModal()}
${renderForecastUpdateConfirmModal()}
${renderTimeEntriesDrawer()}
</div>
</div>
`;
}
function renderMainContent() {
if (appState.status === "loading") {
return `
<main class="arch-panel-extension-main-shell">
<section class="arch-panel-extension-state-card">
<p class="arch-panel-extension-state-eyebrow">Loading data</p>
<h2 class="arch-panel-extension-state-title">Building the workload dataset</h2>
<p class="arch-panel-extension-state-copy">${escapeHtml(appState.loadingMessage)}</p>
<div
class="arch-panel-extension-loading-progress"
role="progressbar"
aria-valuemin="0"
aria-valuemax="100"
aria-valuenow="${Math.round(appState.loadingProgress || 0)}"
>
<div class="arch-panel-extension-loading-progress-meta">
<span>Overall progress</span>
</div>
<div class="arch-panel-extension-loading-progress-track">
<div
class="arch-panel-extension-loading-progress-fill"
style="width: 0%;"
></div>
<span class="arch-panel-extension-loading-progress-value">
${formatProgressPercent(appState.loadingProgress || 0)}
</span>
</div>
</div>
</section>
</main>
`;
}
if (appState.status === "error") {
return `
<main class="arch-panel-extension-main-shell">
<section class="arch-panel-extension-state-card is-error">
<p class="arch-panel-extension-state-eyebrow">Request failed</p>
<h2 class="arch-panel-extension-state-title">Unable to assemble the dashboard</h2>
<p class="arch-panel-extension-state-copy">${escapeHtml(appState.errorMessage)}</p>
<div class="arch-panel-extension-state-actions">
<button type="button" class="arch-panel-extension-utility-button" data-action="refresh">
Retry
</button>
</div>
</section>
</main>
`;
}
if (!appState.dataset) {
return `
<main class="arch-panel-extension-main-shell">
<section class="arch-panel-extension-state-card">
<p class="arch-panel-extension-state-eyebrow">Ready</p>
<h2 class="arch-panel-extension-state-title">Start the live data extraction</h2>
<p class="arch-panel-extension-state-copy">
No cached snapshot is available yet. Use Refresh data to fetch the current user, customers, workloads, actions and service requests.
</p>
<div class="arch-panel-extension-state-actions">
<button type="button" class="arch-panel-extension-utility-button" data-action="refresh">
Refresh data
</button>
</div>
</section>
</main>
`;
}
const { user, summary } = appState.dataset;
return `
<aside class="arch-panel-extension-sidebar${appState.sidebarCollapsed ? " is-collapsed" : ""}" aria-label="Dataset summary">
<div class="arch-panel-extension-sidebar-group">
<p class="arch-panel-extension-sidebar-label">Current user</p>
<div class="arch-panel-extension-sidebar-data">
<strong>${escapeHtml(user.name || user.userEmail)}</strong>
<span>${escapeHtml(user.userEmail)}</span>
<span>Administrator: ${String(Boolean(user.administrator))}</span>
</div>
</div>
<div class="arch-panel-extension-sidebar-group">
<p class="arch-panel-extension-sidebar-label">Exports</p>
<button type="button" class="arch-panel-extension-sidebar-button" data-action="download-json">
Download JSON
</button>
<button type="button" class="arch-panel-extension-sidebar-button" data-action="download-html">
Download HTML
</button>
<button type="button" class="arch-panel-extension-sidebar-button" data-action="refresh">
Refresh data
</button>
</div>
<div class="arch-panel-extension-sidebar-group">
<p class="arch-panel-extension-sidebar-label">Coverage</p>
<div class="arch-panel-extension-sidebar-data">
<span>Customers: ${formatWholeNumber(summary.totalCustomers)}</span>
<span>Workloads: ${formatWholeNumber(summary.totalWorkloads)}</span>
<span>Pending SRs: ${formatWholeNumber(summary.pendingServiceRequests.length)}</span>
</div>
</div>
<div class="arch-panel-extension-sidebar-group">
<p class="arch-panel-extension-sidebar-label">Last refresh</p>
<div class="arch-panel-extension-sidebar-data">
<strong>${escapeHtml(summary.generatedAtLabel || "-")}</strong>
<span>Updated by Refresh data</span>
</div>
</div>
<div class="arch-panel-extension-sidebar-group">
<p class="arch-panel-extension-sidebar-label">Extension version</p>
<div class="arch-panel-extension-sidebar-data">
<span>v${escapeHtml(getExtensionVersion())}</span>
</div>
</div>
</aside>
<main class="arch-panel-extension-main-shell">
<section class="arch-panel-extension-summary-row" aria-label="Overview metrics">
${renderMetricCard("Total clients", formatWholeNumber(summary.totalCustomers), "Loaded from listSummary")}
${renderMetricCard("Total workloads", formatWholeNumber(summary.totalWorkloads), "Excluding Won workloads")}
${renderMetricCard("Total (ACR)", formatCurrency(summary.totalEligibleAcr), "Across eligible workloads")}
${renderMetricCard("Pending SRs", formatWholeNumber(summary.pendingServiceRequests.length), "SVC_PENDING_DELIVERY")}
</section>
<section class="arch-panel-extension-indicator-grid" aria-label="Opportunity indicators">
${summary.stageIndicators.map((indicator) => renderStageIndicator(indicator)).join("")}
</section>
<section class="arch-panel-extension-status-grid" aria-label="SR and action status indicators">
${renderBooleanIndicator(
"Status de SRs",
"Workloads com SR",
summary.srStatus.trueCount,
"Workloads sem SR",
summary.srStatus.falseCount,
"sr"
)}
${renderBooleanIndicator(
"Status de Consumption Plan/action",
"Workloads com action",
summary.actionStatus.trueCount,
"Workloads sem action",
summary.actionStatus.falseCount,
"action"
)}
</section>
${renderSrHoursAndCalendarSection(summary)}
</main>
`;
}
function getExtensionVersion() {
try {
return chrome.runtime.getManifest()?.version || "0.0.0";
} catch {
return "0.0.0";
}
}
function renderMetricCard(label, value, detail) {
return `
<article class="arch-panel-extension-summary-card">
<p class="arch-panel-extension-summary-label">${escapeHtml(label)}</p>
<p class="arch-panel-extension-summary-value">${escapeHtml(value)}</p>
<p class="arch-panel-extension-summary-detail">${escapeHtml(detail)}</p>
</article>
`;
}
function renderStageIndicator(indicator) {
return `
<button
type="button"
class="arch-panel-extension-summary-card arch-panel-extension-summary-card-button"
data-action="open-detail"
data-detail-scope="stage"
data-detail-value="${escapeHtml(indicator.label)}"
>
<p class="arch-panel-extension-summary-label">${escapeHtml(indicator.label)}</p>
<p class="arch-panel-extension-summary-value">${formatWholeNumber(indicator.count)}</p>
<p class="arch-panel-extension-summary-detail">Total (ACR): ${formatCurrency(indicator.totalAcr)}</p>
</button>
`;
}
function renderBooleanIndicator(
title,
trueLabel,
trueCount,
falseLabel,
falseCount,
scope
) {
const total = trueCount + falseCount;
const truePercent = total > 0 ? (trueCount / total) * 100 : 0;
const falsePercent = total > 0 ? (falseCount / total) * 100 : 0;
return `
<article class="arch-panel-extension-panel">
<header class="arch-panel-extension-panel-head">
<div>
<p class="arch-panel-extension-panel-label">${escapeHtml(title)}</p>
</div>
</header>
<div class="arch-panel-extension-progress">
<div class="arch-panel-extension-progress-track">
<span
class="arch-panel-extension-progress-segment is-success"
style="width: ${truePercent}%;"
></span>
<span
class="arch-panel-extension-progress-segment is-danger"
style="width: ${falsePercent}%;"
></span>
</div>
<div class="arch-panel-extension-progress-labels">
<span>${escapeHtml(trueLabel)}: ${formatPercent(truePercent)}</span>
<span>${escapeHtml(falseLabel)}: ${formatPercent(falsePercent)}</span>
</div>
</div>
<div class="arch-panel-extension-boolean-grid">
<button
type="button"
class="arch-panel-extension-summary-card arch-panel-extension-summary-card-button"
data-action="open-detail"
data-detail-scope="${escapeHtml(scope)}"
data-detail-value="true"
>
<p class="arch-panel-extension-summary-label">${escapeHtml(trueLabel)}</p>
<p class="arch-panel-extension-summary-value">${formatWholeNumber(trueCount)}</p>
<p class="arch-panel-extension-summary-detail">${formatPercent(truePercent)}</p>
</button>
<button
type="button"
class="arch-panel-extension-summary-card arch-panel-extension-summary-card-button"
data-action="open-detail"
data-detail-scope="${escapeHtml(scope)}"
data-detail-value="false"
>
<p class="arch-panel-extension-summary-label">${escapeHtml(falseLabel)}</p>
<p class="arch-panel-extension-summary-value">${formatWholeNumber(falseCount)}</p>
<p class="arch-panel-extension-summary-detail">${formatPercent(falsePercent)}</p>
</button>
</div>
</article>
`;
}
function renderSrHoursAndCalendarSection(summary) {
const calendar = buildWorkloadStartCalendar(
summary.allWorkloads,
appState.calendarYear,
appState.calendarMonth,
appState.calendarView,
appState.calendarAnchorDateKey
);
return `
<section class="arch-panel-extension-operations-grid" aria-label="Pending SRs and workload calendar">
${renderSrHoursIndicatorCard(summary)}
${renderWorkloadStartCalendar(calendar)}
</section>
`;
}
function renderSrHoursIndicatorCard(summary) {
const zeroToFourCount = summary.srHours.betweenZeroAndFour.length;
const aboveFourCount = summary.srHours.aboveFour.length;
const total = zeroToFourCount + aboveFourCount;
const zeroToFourPercent = total > 0 ? (zeroToFourCount / total) * 100 : 0;
const aboveFourPercent = total > 0 ? (aboveFourCount / total) * 100 : 0;
return `
<article class="arch-panel-extension-panel">
<header class="arch-panel-extension-panel-head">
<div>
<p class="arch-panel-extension-panel-label">Horas apontadas</p>
</div>
</header>
<div class="arch-panel-extension-progress">
<div class="arch-panel-extension-progress-track">
<span
class="arch-panel-extension-progress-segment is-success"
style="width: ${zeroToFourPercent}%;"
></span>
<span
class="arch-panel-extension-progress-segment is-danger"
style="width: ${aboveFourPercent}%;"
></span>
</div>
<div class="arch-panel-extension-progress-labels">
<span>SRs 0-4h: ${formatPercent(zeroToFourPercent)}</span>
<span>SRs &gt;4h: ${formatPercent(aboveFourPercent)}</span>
</div>
</div>
<div class="arch-panel-extension-boolean-grid">
${renderSrHoursIndicator(
"SRs 0-4h",
formatPercent(zeroToFourPercent),
zeroToFourCount,
"0-4"
)}
${renderSrHoursIndicator(
"SRs >4h",
formatPercent(aboveFourPercent),
aboveFourCount,
"above-4"
)}
</div>
</article>
`;
}
function renderSrHoursIndicator(label, detail, count, value) {
return `
<button
type="button"
class="arch-panel-extension-summary-card arch-panel-extension-summary-card-button"
data-action="open-detail"
data-detail-scope="sr-hours"
data-detail-value="${escapeHtml(value)}"
aria-label="Open ${escapeHtml(label)} service requests"
>
<p class="arch-panel-extension-summary-label">${escapeHtml(label)}</p>
<p class="arch-panel-extension-summary-value">${formatWholeNumber(count)}</p>
<p class="arch-panel-extension-summary-detail">${escapeHtml(detail)}</p>
</button>
`;
}
function renderWorkloadStartCalendar(calendar) {
return `
<article class="arch-panel-extension-panel arch-panel-extension-calendar-card">
<header class="arch-panel-extension-panel-head">
<div>
<p class="arch-panel-extension-panel-label">Workload start calendar</p>
<h3 class="arch-panel-extension-panel-title">${escapeHtml(calendar.periodLabel)}</h3>
</div>
<div class="arch-panel-extension-calendar-actions">
<div class="arch-panel-extension-calendar-view-toggle" aria-label="Calendar view">
${["month", "week"]
.map(
(view) => `
<button
type="button"
class="arch-panel-extension-calendar-view-button${
calendar.view === view ? " is-active" : ""
}"
data-action="calendar-view"
data-calendar-view="${view}"
aria-pressed="${calendar.view === view ? "true" : "false"}"
>
${view === "month" ? "Month" : "Week"}
</button>
`
)
.join("")}
</div>
<button
type="button"
class="arch-panel-extension-calendar-nav"
data-action="calendar-prev"
aria-label="${calendar.view === "week" ? "Previous week" : "Previous month"}"
>
</button>
<span>${formatWholeNumber(calendar.totalVisibleWorkloads)} starts</span>
<button
type="button"
class="arch-panel-extension-calendar-today"
data-action="calendar-today"
aria-label="${calendar.view === "week" ? "Go to current week" : "Go to current month"}"
>
Today
</button>
<button
type="button"
class="arch-panel-extension-calendar-nav"
data-action="calendar-next"
aria-label="${calendar.view === "week" ? "Next week" : "Next month"}"
>
</button>
</div>
</header>
<div class="arch-panel-extension-calendar-weekdays${
calendar.view === "week" ? " is-week" : ""
}">
${["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]
.map((day) => `<span>${day}</span>`)
.join("")}
</div>
<div class="arch-panel-extension-calendar-grid${
calendar.view === "week" ? " is-week" : ""
}">
${calendar.days
.map((day) => renderCalendarDay(day, calendar.view))
.join("")}
</div>
</article>
`;
}
function renderCalendarDay(day, view = "month") {
if (view === "week") {
return renderCalendarWeekDay(day);
}
const firstWorkload = day.workloads[0];
const calendarWorkloadLabel =
firstWorkload?.description || firstWorkload?.name || "Workload";
const className = [
"arch-panel-extension-calendar-day",
day.isCurrentMonth ? "" : "is-muted",
day.isToday ? "is-today" : "",
day.workloads.length ? "has-workload" : "",
]
.filter(Boolean)
.join(" ");
return `
<button
type="button"
class="${className}"
title="${escapeHtml(
day.workloads
.map((workload) => workload.description || workload.name)
.filter(Boolean)
.join(" | ")
)}"
data-action="open-detail"
data-detail-scope="calendar-day"
data-detail-value="${escapeHtml(day.dateKey)}"
${day.workloads.length ? "" : "disabled"}
>
<span class="arch-panel-extension-calendar-date">${day.dayNumber}</span>
${
day.isWeekStart
? `<span class="arch-panel-extension-calendar-week-number">${escapeHtml(
day.operationalWeekLabel
)}</span>`
: ""
}
${
day.workloads.length
? `<span class="arch-panel-extension-calendar-workload">${escapeHtml(
calendarWorkloadLabel
)}</span><span class="arch-panel-extension-calendar-count">${formatWholeNumber(
day.workloads.length
)}</span>`
: ""
}
</button>
`;
}
function renderCalendarWeekDay(day) {
const className = [
"arch-panel-extension-calendar-day",
"is-week-day",
day.isToday ? "is-today" : "",
day.workloads.length ? "has-workload" : "",
]
.filter(Boolean)
.join(" ");
const title = day.workloads
.map((workload) => workload.description || workload.name)
.filter(Boolean)
.join(" | ");
return `
<button
type="button"
class="${className}"
title="${escapeHtml(title)}"
data-action="open-detail"
data-detail-scope="calendar-day"
data-detail-value="${escapeHtml(day.dateKey)}"
${day.workloads.length ? "" : "disabled"}
>
<span class="arch-panel-extension-calendar-week-day-head">
<span class="arch-panel-extension-calendar-date">${day.dayNumber}</span>
${
day.isWeekStart
? `<span class="arch-panel-extension-calendar-week-number">${escapeHtml(
day.operationalWeekLabel
)}</span>`
: ""
}
</span>
<span class="arch-panel-extension-calendar-week-workloads">
${
day.workloads.length
? day.workloads.map((workload) => renderCalendarWeekWorkload(workload)).join("")
: `<span class="arch-panel-extension-calendar-week-empty">No starts</span>`
}
</span>
</button>
`;
}
function renderCalendarWeekWorkload(workload) {
const label = workload.description || workload.name || "Workload";
const customer = workload.customerName || "";
return `
<span class="arch-panel-extension-calendar-week-workload">
<span class="arch-panel-extension-calendar-week-workload-title">${escapeHtml(label)}</span>
${
customer
? `<span class="arch-panel-extension-calendar-week-workload-meta">${escapeHtml(
customer
)}</span>`
: ""
}
</span>
`;
}
function renderWorkloadTable(workloads) {
return `
<div class="arch-panel-extension-table-wrap is-main">
<table class="arch-panel-extension-table">
<colgroup>
<col class="arch-panel-extension-col-opportunity">
<col class="arch-panel-extension-col-customer">
<col class="arch-panel-extension-col-workload">
<col class="arch-panel-extension-col-acr">
<col class="arch-panel-extension-col-type">
<col class="arch-panel-extension-col-flag">
<col class="arch-panel-extension-col-flag">
</colgroup>
<thead>
<tr>
<th>Opportunity</th>
<th>Customer</th>
<th>Workload</th>
<th class="is-numeric">ACR</th>
<th>Type</th>
<th>Tem SR?</th>
<th>Tem Action?</th>
</tr>
</thead>
<tbody>
${workloads.map((workload) => renderWorkloadTableRow(workload)).join("")}
</tbody>
</table>
</div>
`;
}
function renderWorkloadTableRow(
workload,
includeConsumptionStartDate = false,
includeRampMonths = false
) {
const hasRampMismatch =
includeRampMonths && hasRampForecastMismatch(workload);
return `
<tr>
<td>${renderOpportunityLink(workload)}</td>
<td>${escapeHtml(workload.customerName || "")}</td>
<td>${renderWorkloadLink(workload, hasRampMismatch)}</td>
${
includeConsumptionStartDate
? `<td>${escapeHtml(formatDate(workload.consumptionStartDate) || "-")}</td>`
: ""
}
${
includeRampMonths
? `<td class="is-numeric">${renderRampMonthsButton(workload)}</td>`
: ""
}
<td class="is-numeric">${formatCurrency(workload.adjustedACR)}</td>
<td>${escapeHtml(workload.opportunityForecastTypeGroup)}</td>
<td>${renderBooleanBadge(workload.hasSR)}</td>
<td>${renderHasActionCell(workload)}</td>
</tr>
`;
}
function renderRampMonthsButton(workload) {
return `
<button
type="button"
class="arch-panel-extension-row-link arch-panel-extension-inline-link-button"
data-action="open-ramp-comparison"
data-workload-id="${escapeHtml(workload.workloadId || "")}"
aria-label="Abrir comparação de rampa para ${escapeHtml(
workload.name || "workload"
)}"
>
${formatWholeNumber(workload.rampMonths || 0)}
</button>
`;
}
function renderBooleanBadge(value) {
const isTrue = Boolean(value);
const variantClass = isTrue ? "is-success" : "is-danger";
const label = isTrue ? "Sim" : "Não";
return `
<span class="arch-panel-extension-badge ${variantClass}">
${label}
</span>
`;
}
function renderHasActionCell(workload) {
if (workload.hasAction) {
return renderBooleanBadge(true);
}
return `
<div class="arch-panel-extension-action-cell">
${renderBooleanBadge(false)}
<button
type="button"
class="arch-panel-extension-add-action-button"
data-action="open-action-form"
data-workload-id="${escapeHtml(workload.workloadId || "")}"
aria-label="Criar action para ${escapeHtml(workload.name || "workload")}"
title="Criar action"
>
</button>
</div>
`;
}
function renderInsightRow(label, value, detail) {
return `
<div class="arch-panel-extension-insight">
<div>
<p class="arch-panel-extension-insight-label">${escapeHtml(label)}</p>
<p class="arch-panel-extension-insight-detail">${escapeHtml(detail)}</p>
</div>
<p class="arch-panel-extension-insight-value">${escapeHtml(value)}</p>
</div>
`;
}
function renderDetailModal() {
if (!appState.detailModal) {
return "";
}
const isServiceRequestDetail = appState.detailModal.type === "serviceRequests";
const detailItems = isServiceRequestDetail
? appState.detailModal.items || []
: getSortedDetailItems();
return `
<div class="arch-panel-extension-detail-overlay">
<div class="arch-panel-extension-detail-dialog" role="document">
<header class="arch-panel-extension-detail-head">
<div>
<p class="arch-panel-extension-panel-label">${escapeHtml(appState.detailModal.label)}</p>
<h3 class="arch-panel-extension-panel-title">${escapeHtml(appState.detailModal.title)}</h3>
</div>
</header>
<div class="arch-panel-extension-detail-meta">
${
isServiceRequestDetail
? `<span>${formatWholeNumber(detailItems.length)} SRs</span>`
: `<span>${formatWholeNumber(detailItems.length)} workloads</span>
<span class="arch-panel-extension-panel-dot"></span>
<span>Total (ACR): ${formatCurrency(appState.detailModal.totalAcr)}</span>`
}
</div>
${
isServiceRequestDetail
? renderServiceRequestDetailTable(detailItems)
: renderDetailTable(
detailItems,
appState.detailModal.scope === "calendar-day"
)
}
<div class="arch-panel-extension-detail-actions">
<button
type="button"
class="arch-panel-extension-close"
data-action="close-detail"
aria-label="Fechar detalhamento"
>
Fechar
</button>
</div>
</div>
</div>
`;
}
function renderActionFormModal() {
if (!appState.actionFormModal || !appState.dataset) {
return "";
}
const workload = findWorkloadById(appState.actionFormModal.workloadId);
const userEmail = appState.dataset.user?.userEmail || "";
if (!workload) {
return "";
}
const modalError = cleanString(appState.actionFormModal.errorMessage);
const isSubmitting = Boolean(appState.actionFormModal.isSubmitting);
return `
<div class="arch-panel-extension-detail-overlay">
<div class="arch-panel-extension-action-dialog" role="document">
<header class="arch-panel-extension-detail-head">
<div>
<p class="arch-panel-extension-panel-label">Create workload action</p>
<h3 class="arch-panel-extension-panel-title">Criar action</h3>
</div>
<button
type="button"
class="arch-panel-extension-close"
data-action="close-action-form"
aria-label="Fechar formulário"
>
Fechar
</button>
</header>
<div class="arch-panel-extension-detail-meta">
<span>${renderOpportunityLink(workload)}</span>
<span class="arch-panel-extension-panel-dot"></span>
<span>${escapeHtml(workload.customerName || "")}</span>
<span class="arch-panel-extension-panel-dot"></span>
<span>${escapeHtml(workload.name || "Unnamed workload")}</span>
</div>
${modalError ? `<p class="arch-panel-extension-form-error">${escapeHtml(modalError)}</p>` : ""}
<form class="arch-panel-extension-action-form">
<input type="hidden" name="parentType" value="WORKLOAD">
<input type="hidden" name="parentId" value="${escapeHtml(workload.workloadId || "")}">
<input type="hidden" name="ownerEmail" value="${escapeHtml(userEmail)}">
<input type="hidden" name="role" value="CLOUD_ARCHITECT">
<input type="hidden" name="name" value="SUCCESS_PLAN_CONSUM_RVW">
<input type="hidden" name="status" value="COMPLETE">
<input type="hidden" name="notes" value="[]">
<input type="hidden" name="objective" value="">
<input type="hidden" name="createdBy" value="${escapeHtml(userEmail)}">
<input type="hidden" name="updatedBy" value="${escapeHtml(userEmail)}">
<div class="arch-panel-extension-form-grid">
<label class="arch-panel-extension-form-field">
<span>Team</span>
<select name="team" required ${isSubmitting ? "disabled" : ""}>
<option value="">Selecione</option>
${ACTION_TEAM_OPTIONS.map(
(option) =>
`<option value="${escapeHtml(option.value)}">${escapeHtml(option.label)}</option>`
).join("")}
</select>
</label>
<label class="arch-panel-extension-form-field">
<span>Data de início</span>
<input type="date" name="startDate" required ${isSubmitting ? "disabled" : ""}>
</label>
<label class="arch-panel-extension-form-field">
<span>Data fim</span>
<input type="date" name="endDate" required ${isSubmitting ? "disabled" : ""}>
</label>
<label class="arch-panel-extension-form-field">
<span>Complexity</span>
<select name="complexity" required ${isSubmitting ? "disabled" : ""}>
<option value="">Selecione</option>
${ACTION_COMPLEXITY_OPTIONS.map(
(option) =>
`<option value="${escapeHtml(option)}">${escapeHtml(option)}</option>`
).join("")}
</select>
</label>
</div>
<div class="arch-panel-extension-detail-actions">
<button
type="button"
class="arch-panel-extension-close"
data-action="close-action-form"
aria-label="Fechar formulário"
${isSubmitting ? "disabled" : ""}
>
Fechar
</button>
<button
type="button"
class="arch-panel-extension-utility-button"
data-action="submit-action-form"
${isSubmitting ? "disabled" : ""}
>
${isSubmitting ? "Criando..." : "Criar action"}
</button>
</div>
</form>
</div>
</div>
`;
}
function renderRampComparisonModal() {
const modal = appState.rampComparisonModal;
if (!modal) {
return "";
}
const rows = modal.rows || [];
const title = `${modal.workload.customerName || "Unknown customer"} | ${
modal.workload.name || "Unnamed workload"
}${
modal.workload.description ? ` | ${modal.workload.description}` : ""
}`;
return `
<div class="arch-panel-extension-detail-overlay">
<div class="arch-panel-extension-ramp-dialog" role="document">
<header class="arch-panel-extension-detail-head">
<div>
<p class="arch-panel-extension-panel-label">Ramp forecast comparison</p>
<h3 class="arch-panel-extension-panel-title">${escapeHtml(title)}</h3>
</div>
</header>
<div class="arch-panel-extension-detail-meta">
<span>Ramp months: ${formatWholeNumber(modal.workload.rampMonths || 0)}</span>
<span class="arch-panel-extension-panel-dot"></span>
<span>Adjusted ACR: ${formatCurrency(modal.workload.adjustedACR)}</span>
<span class="arch-panel-extension-panel-dot"></span>
<span>Consumption start: ${escapeHtml(
formatDate(modal.workload.consumptionStartDate) || "-"
)}</span>
</div>
${renderRampComparisonPivotTable(rows)}
<div class="arch-panel-extension-detail-actions">
<button
type="button"
class="arch-panel-extension-utility-button is-primary"
data-action="open-forecast-update-confirm"
${rows.length ? "" : "disabled"}
>
Atualizar pelo Calculated ramp
</button>
<button
type="button"
class="arch-panel-extension-close"
data-action="close-ramp-comparison"
aria-label="Fechar comparação de rampa"
>
Fechar
</button>
</div>
</div>
</div>
`;
}
function renderRampComparisonPivotTable(rows) {
if (!rows.length) {
return `<div class="arch-panel-extension-empty-state">Nenhum forecast encontrado para comparar.</div>`;
}
return `
<div class="arch-panel-extension-table-wrap is-ramp-pivot">
<table class="arch-panel-extension-table arch-panel-extension-ramp-pivot-table">
<tbody>
<tr>
<th>Period</th>
${rows.map((row) => `<td>${escapeHtml(row.periodLabel)}</td>`).join("")}
</tr>
<tr>
<th>Current adjusted</th>
${rows
.map(
(row) =>
`<td class="is-numeric">${formatCurrency(row.currentValue)}</td>`
)
.join("")}
</tr>
<tr>
<th>Calculated ramp</th>
${rows
.map(
(row) =>
`<td class="is-numeric">${formatCurrency(row.calculatedValue)}</td>`
)
.join("")}
</tr>
<tr>
<th>Delta</th>
${rows
.map(
(row) =>
`<td class="is-numeric${
isNonZeroAmount(row.delta) ? " is-delta-different" : ""
}">${formatCurrency(row.delta)}</td>`
)
.join("")}
</tr>
</tbody>
</table>
</div>
`;
}
function renderForecastUpdateConfirmModal() {
const modal = appState.forecastUpdateConfirmModal;
if (!modal) {
return "";
}
return `
<div class="arch-panel-extension-detail-overlay is-confirmation">
<div class="arch-panel-extension-confirm-dialog" role="document">
<header class="arch-panel-extension-detail-head">
<div>
<p class="arch-panel-extension-panel-label">Confirmar atualização</p>
<h3 class="arch-panel-extension-panel-title">Atualizar forecast pelo Calculated ramp?</h3>
</div>
</header>
<p class="arch-panel-extension-detail-subtitle">
Esta ação enviará ${formatWholeNumber(
modal.payload.length
)} meses calculados para o workload atual. Os valores atuais do forecast serão substituídos pelos valores da linha Calculated ramp.
</p>
${
modal.errorMessage
? `<p class="arch-panel-extension-form-error">${escapeHtml(
modal.errorMessage
)}</p>`
: ""
}
<div class="arch-panel-extension-detail-actions">
<button
type="button"
class="arch-panel-extension-close"
data-action="close-forecast-update-confirm"
${modal.isSubmitting ? "disabled" : ""}
>
Cancelar
</button>
<button
type="button"
class="arch-panel-extension-utility-button is-primary"
data-action="submit-forecast-update"
${modal.isSubmitting ? "disabled" : ""}
>
${modal.isSubmitting ? "Atualizando..." : "Confirmar atualização"}
</button>
</div>
</div>
</div>
`;
}
function renderTimeEntriesDrawer() {
const drawer = appState.timeEntriesDrawer;
if (!drawer) {
return "";
}
const items = Array.isArray(drawer.items) ? drawer.items : [];
return `
<aside class="arch-panel-extension-time-drawer" aria-label="Time entries summary">
<header class="arch-panel-extension-time-drawer-head">
<div>
<p class="arch-panel-extension-panel-label">Horas apontadas</p>
<h3 class="arch-panel-extension-panel-title">SR ${escapeHtml(drawer.srNumber || "-")}</h3>
</div>
</header>
${renderTimeEntriesDrawerBody(drawer, items)}
<footer class="arch-panel-extension-time-drawer-actions">
<button
type="button"
class="arch-panel-extension-close arch-panel-extension-time-drawer-close"
data-action="close-time-entries"
aria-label="Fechar apontamentos de horas"
>
Fechar
</button>
</footer>
</aside>
`;
}
function renderTimeEntriesDrawerBody(drawer, items) {
if (drawer.status === "loading") {
return `
<div class="arch-panel-extension-time-drawer-state">
<p class="arch-panel-extension-summary-label">Carregando</p>
<p class="arch-panel-extension-summary-value">Buscando dados</p>
<p class="arch-panel-extension-summary-detail">Consultando o resumo de time entries na origem COMCIP.</p>
</div>
`;
}
if (drawer.status === "error") {
return `
<div class="arch-panel-extension-time-drawer-state is-error">
<p class="arch-panel-extension-summary-label">Erro na requisição</p>
<p class="arch-panel-extension-detail-subtitle">${escapeHtml(drawer.errorMessage || "Não foi possível carregar os apontamentos.")}</p>
<button type="button" class="arch-panel-extension-utility-button" data-action="retry-time-entries">
Tentar novamente
</button>
</div>
`;
}
const groupedItems = groupTimeEntriesByResourceName(items);
return `
<div class="arch-panel-extension-time-drawer-meta">
<span>${formatWholeNumber(items.length)} registros</span>
<span>${formatWholeNumber(groupedItems.length)} resources</span>
</div>
${
items.length
? groupedItems.map((group) => renderTimeEntryGroup(group)).join("")
: `<div class="arch-panel-extension-empty-state">Nenhum registro retornado para esta SR.</div>`
}
`;
}
function renderTimeEntryGroup(group) {
const totalTimeSpent = group.items.reduce((sum, item) => {
return sum + getTimeEntryTimeSpent(item);
}, 0);
return `
<section class="arch-panel-extension-time-entry-group">
<header class="arch-panel-extension-time-entry-group-head">
<div>
<p class="arch-panel-extension-summary-label">resourceName</p>
<h4>${escapeHtml(group.resourceName)}</h4>
</div>
<span>${formatHours(totalTimeSpent)} total</span>
</header>
<div class="arch-panel-extension-time-entry-table-wrap">
<table class="arch-panel-extension-time-entry-table">
<thead>
<tr>
<th>Activity</th>
<th>Task type</th>
<th class="is-numeric">Time spent</th>
</tr>
</thead>
<tbody>
${group.items.map((item) => renderTimeEntryRow(item)).join("")}
</tbody>
<tfoot>
<tr>
<th colspan="2">Total</th>
<th class="is-numeric">${formatHours(totalTimeSpent)}</th>
</tr>
</tfoot>
</table>
</div>
</section>
`;
}
function renderTimeEntryRow(item) {
return `
<tr>
<td>${escapeHtml(getTimeEntryActivity(item) || "-")}</td>
<td>${escapeHtml(getTimeEntryTaskType(item) || "-")}</td>
<td class="is-numeric">${formatHours(getTimeEntryTimeSpent(item))}</td>
</tr>
`;
}
function renderDetailTable(items, includeConsumptionStartDate = false) {
return `
<div class="arch-panel-extension-table-wrap is-detail">
<table class="arch-panel-extension-table">
<thead>
<tr>
<th>${renderSortButton("Opportunity", DETAIL_SORT_KEYS.opportunityNumber)}</th>
<th>${renderSortButton("Customer", DETAIL_SORT_KEYS.customerName)}</th>
<th>${renderSortButton("Workload", DETAIL_SORT_KEYS.workload)}</th>
${includeConsumptionStartDate ? "<th>consumptionStartDate</th>" : ""}
<th class="is-numeric">${renderSortButton("Ramp months", DETAIL_SORT_KEYS.rampMonths, true)}</th>
<th class="is-numeric">${renderSortButton("ACR", DETAIL_SORT_KEYS.adjustedACR, true)}</th>
<th>${renderSortButton("Type", DETAIL_SORT_KEYS.opportunityForecastTypeGroup)}</th>
<th>${renderSortButton("Tem SR?", DETAIL_SORT_KEYS.hasSR)}</th>
<th>${renderSortButton("Tem Action?", DETAIL_SORT_KEYS.hasAction)}</th>
</tr>
</thead>
<tbody>
${items
.map((workload) =>
renderWorkloadTableRow(workload, includeConsumptionStartDate, true)
)
.join("")}
</tbody>
</table>
</div>
`;
}
function buildRampForecastComparisonRows(workloads) {
return (workloads || [])
.flatMap((workload) => {
const startDate = parseDatePreservingDateOnly(workload.consumptionStartDate);
if (Number.isNaN(startDate.getTime())) {
return [];
}
const startMonth = startDate.getMonth() + 1;
const startYear = startDate.getFullYear();
const workloadLabel = `${workload.name || "Unnamed workload"}${
workload.description ? ` | ${workload.description}` : ""
}`;
return (workload.forecast || [])
.map((forecastItem) => {
const forecastMonth = parseForecastMonthNumber(forecastItem.month);
const forecastYear = toNumber(forecastItem.year);
if (!forecastMonth || !forecastYear) {
return null;
}
const monthIndex =
(forecastYear - startYear) * 12 + (forecastMonth - startMonth) + 1;
const currentValue = toNumber(forecastItem.adjustedConsumptionAmount);
const calculatedValue =
monthIndex >= 1 && monthIndex <= 12
? calculateRampForecastAmount({
adjustedACR: workload.adjustedACR,
rampMonths: workload.rampMonths,
monthIndex,
consumptionStartDate: startDate,
})
: 0;
return {
workloadLabel,
periodLabel: formatForecastPeriodLabel(forecastYear, forecastMonth),
currentValue,
calculatedValue,
delta: currentValue - calculatedValue,
monthIndex,
year: forecastYear,
month: forecastMonth,
sortKey: `${workloadLabel}|${String(forecastYear).padStart(
4,
"0"
)}-${String(forecastMonth).padStart(2, "0")}`,
};
})
.filter(Boolean);
})
.sort((left, right) => left.sortKey.localeCompare(right.sortKey));
}
function hasRampForecastMismatch(workload) {
return buildRampForecastComparisonRows([workload]).some((row) =>
isNonZeroAmount(row.delta)
);
}
function calculateRampForecastAmount({
adjustedACR,
rampMonths,
monthIndex,
consumptionStartDate,
}) {
const annualValue = toNumber(adjustedACR);
const ramp = Math.max(0, toNumber(rampMonths));
const month = Math.max(1, toNumber(monthIndex));
if (!annualValue) {
return 0;
}
if (ramp <= 0) {
return Math.round(
(annualValue / 12) *
getStartMonthProrationFactor(month, consumptionStartDate)
);
}
const steadyStateMonthlyAmount = (2 * annualValue) / (24 - ramp);
const rampStepCount = ramp + 1;
const calculatedAmount =
month <= rampStepCount
? steadyStateMonthlyAmount * (month / rampStepCount)
: steadyStateMonthlyAmount;
return Math.round(
calculatedAmount * getStartMonthProrationFactor(month, consumptionStartDate)
);
}
function getStartMonthProrationFactor(monthIndex, consumptionStartDate) {
if (toNumber(monthIndex) !== 1) {
return 1;
}
const startDate = parseDatePreservingDateOnly(consumptionStartDate);
if (Number.isNaN(startDate.getTime())) {
return 1;
}
const daysInMonth = new Date(
startDate.getFullYear(),
startDate.getMonth() + 1,
0
).getDate();
const remainingDays = daysInMonth - startDate.getDate() + 1;
return Math.max(0, Math.min(1, remainingDays / daysInMonth));
}
function isNonZeroAmount(value) {
return Math.abs(toNumber(value)) > 0.000001;
}
function parseForecastMonthNumber(value) {
if (typeof value === "number") {
return value >= 1 && value <= 12 ? value : 0;
}
const normalized = normalizeString(value);
const numericMonth = Number.parseInt(normalized, 10);
if (numericMonth >= 1 && numericMonth <= 12) {
return numericMonth;
}
const monthMap = {
JAN: 1,
JANUARY: 1,
FEB: 2,
FEBRUARY: 2,
MAR: 3,
MARCH: 3,
APR: 4,
APRIL: 4,
MAY: 5,
JUN: 6,
JUNE: 6,
JUL: 7,
JULY: 7,
AUG: 8,
AUGUST: 8,
SEP: 9,
SEPT: 9,
SEPTEMBER: 9,
OCT: 10,
OCTOBER: 10,
NOV: 11,
NOVEMBER: 11,
DEC: 12,
DECEMBER: 12,
};
return monthMap[normalized] || 0;
}
function formatForecastPeriodLabel(year, month) {
const fiscalYear = month >= 6 ? year + 1 : year;
const fiscalYearLabel = formatWeekNumber(fiscalYear % 100);
const monthLabel = new Intl.DateTimeFormat("en-US", {
month: "short",
}).format(new Date(year, month - 1, 1));
return `FY${fiscalYearLabel} ${monthLabel}`;
}
function renderServiceRequestDetailTable(items) {
return `
<div class="arch-panel-extension-table-wrap is-detail">
<table class="arch-panel-extension-table arch-panel-extension-sr-detail-table">
<thead>
<tr>
<th>Customer</th>
<th>SR Number</th>
<th>Queue</th>
<th>Service</th>
<th>Pillar</th>
<th class="is-numeric">Oppty value</th>
<th class="is-numeric">Horas apontadas</th>
<th>Customer deadline</th>
<th>Oppty Status</th>
</tr>
</thead>
<tbody>
${items
.map(
(serviceRequest) => `
<tr>
<td>${escapeHtml(serviceRequest.customerName || "-")}</td>
<td>${renderServiceRequestLink(serviceRequest)}</td>
<td>${escapeHtml(serviceRequest.queueName || "-")}</td>
<td>${escapeHtml(serviceRequest.serviceName || "-")}</td>
<td>${escapeHtml(serviceRequest.pillarLabel || "-")}</td>
<td class="is-numeric">${formatCurrency(serviceRequest.opportunityValue)}</td>
<td class="is-numeric">${renderReportedHoursLink(serviceRequest)}</td>
<td>${escapeHtml(formatDate(serviceRequest.customerDeadline) || "-")}</td>
<td>${escapeHtml(serviceRequest.opportunityStatusLabel || "-")}</td>
</tr>
`
)
.join("")}
</tbody>
</table>
</div>
`;
}
function renderSortButton(label, sortKey, isNumeric = false) {
const currentKey = appState.detailModal?.sortKey || "";
const currentDirection = appState.detailModal?.sortDirection || "asc";
const indicator =
currentKey === sortKey ? (currentDirection === "asc" ? " ↑" : " ↓") : "";
return `
<button
type="button"
class="arch-panel-extension-sort-button${isNumeric ? " is-numeric" : ""}"
data-action="sort-detail"
data-sort-key="${escapeHtml(sortKey)}"
aria-label="Sort by ${escapeHtml(label)}"
>
<span>${escapeHtml(label)}${indicator}</span>
</button>
`;
}
function renderOpportunityLink(workload) {
const label = workload.opportunityNumber || "Opportunity";
if (!workload.opportunityId || !workload.opportunityNumber) {
return escapeHtml(label);
}
const href = `https://eeho.fa.us2.oraclecloud.com/fscmUI/redwood/cx-sales/application/container/opportunities/opportunities-detail?id=${encodeURIComponent(
workload.opportunityId
)}&puid=${encodeURIComponent(workload.opportunityNumber)}`;
return `<a class="arch-panel-extension-row-link" href="${href}" target="_blank" rel="noreferrer">${escapeHtml(label)}</a>`;
}
function renderServiceRequestLink(serviceRequest) {
const srNumber = cleanString(serviceRequest?.srNumber);
if (!srNumber) {
return "-";
}
const href = `https://eeho.fa.us2.oraclecloud.com/fscmUI/redwood/service/ec/container/sr/edit?srNumber=${encodeURIComponent(
srNumber
)}`;
return `<a class="arch-panel-extension-row-link" href="${href}" target="_blank" rel="noreferrer">${escapeHtml(srNumber)}</a>`;
}
function renderReportedHoursLink(serviceRequest) {
const srNumber = cleanString(serviceRequest?.srNumber);
const label = formatHours(serviceRequest?.totalHoursWorked);
if (!srNumber) {
return escapeHtml(label);
}
return `
<button
type="button"
class="arch-panel-extension-row-link arch-panel-extension-inline-link-button"
data-action="open-time-entries"
data-sr-number="${escapeHtml(srNumber)}"
aria-label="Abrir apontamentos de horas da SR ${escapeHtml(srNumber)}"
>
${escapeHtml(label)}
</button>
`;
}
function renderWorkloadLink(workload, showRampAlert = false) {
const description = workload.description ? ` | ${workload.description}` : "";
const label = `${workload.name || "Unnamed workload"}${description}`;
if (!workload.workloadId) {
return `${escapeHtml(label)}${renderRampAlertButton(workload, showRampAlert)}`;
}
const href = `https://spa.oracle.com/oalcrm/web/api/g2m-consumer-application/ui/index.html?ojr=workload_workbench/workload_details;workloadId=${encodeURIComponent(
workload.workloadId
)}`;
return `
<span class="arch-panel-extension-workload-link-cell">
<a class="arch-panel-extension-row-link" href="${href}" target="_blank" rel="noreferrer">${escapeHtml(label)}</a>
${renderRampAlertButton(workload, showRampAlert)}
</span>
`;
}
function renderRampAlertButton(workload, showRampAlert) {
if (!showRampAlert || !workload.workloadId) {
return "";
}
return `
<button
type="button"
class="arch-panel-extension-ramp-alert-button"
data-action="open-ramp-comparison"
data-workload-id="${escapeHtml(workload.workloadId || "")}"
title="Requer atenção: existe Delta diferente de zero"
aria-label="Abrir comparação de rampa com inconsistência para ${escapeHtml(
workload.name || "workload"
)}"
>
!
</button>
`;
}
function getHeaderNote() {
if (appState.status === "loading") {
return appState.loadingMessage || "Loading";
}
if (appState.status === "error") {
return "Extraction failed";
}
if (appState.dataset) {
return `${appState.dataset.user.userEmail} · ${formatWholeNumber(
appState.dataset.summary.eligibleWorkloads.length
)} eligible workloads`;
}
return "Ready";
}
function getHeaderSummary() {
if (appState.status === "loading") {
return appState.loadingMessage || "Loading";
}
if (appState.status === "error") {
return "Extraction failed";
}
if (appState.dataset) {
return `${appState.dataset.user.userEmail} | ${formatWholeNumber(
appState.dataset.summary.eligibleWorkloads.length
)} eligible workloads`;
}
return "Ready";
}
function openModal() {
const overlay = ensureModal();
if (!overlay) {
return;
}
window.clearTimeout(closeTimer);
restoreFocusTarget =
document.activeElement instanceof HTMLElement ? document.activeElement : null;
overlay.hidden = false;
overlay.setAttribute("aria-hidden", "false");
lockScroll();
window.requestAnimationFrame(() => {
overlay.classList.add(CLASSES.overlayOpen);
const primaryAction = overlay.querySelector("[data-action='close']");
primaryAction?.focus();
});
if (!appState.dataset && appState.status !== "loading") {
void hydrateDatasetFromCache();
}
}
function closeModal() {
const overlay = document.getElementById(IDS.overlay);
if (!overlay || overlay.hidden) {
return;
}
overlay.classList.remove(CLASSES.overlayOpen);
overlay.setAttribute("aria-hidden", "true");
unlockScroll();
appState.detailModal = null;
appState.actionFormModal = null;
appState.rampComparisonModal = null;
appState.forecastUpdateConfirmModal = null;
appState.timeEntriesDrawer = null;
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("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.",
});
}
function sendComcipRequestMessage({ method, url, payload, timeoutMessage }) {
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,
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 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 payload = await fetchJson(API_ENDPOINTS.currentUser);
const userPayload = payload?.user ?? payload;
const userEmail = cleanString(userPayload?.id);
appState.sessionAuthHeaders = extractAuthHeaders(payload?.token);
if (!userEmail) {
throw new Error(
`Unable to resolve userEmail from users/current.user.id. Available top-level keys: ${Object.keys(
payload || {}
).join(", ")}`
);
}
return {
userEmail,
name:
firstNonEmptyString([
userPayload?.name,
userPayload?.displayName,
userPayload?.fullName,
userPayload?.profile?.name,
]) || userEmail,
administrator: Boolean(userPayload?.administrator),
};
}
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 (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 = cleanString(url);
if (!normalized) {
return false;
}
try {
const parsedUrl = new URL(normalized, window.location.origin);
return parsedUrl.origin !== window.location.origin;
} catch {
return false;
}
}
async function fetchJsonViaExtension(url, options = {}) {
const requestUrl = new URL(url, window.location.origin).toString();
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 combinedHeaders;
}
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,
]) || "",
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."));
};
});
}
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 wwbAuthorization =
appState.sessionAuthHeaders?.["wwb-provider-authorization"] ||
appState.sessionAuthHeaders?.["wwb-provider-Authorization"] ||
"";
const headers = {
accept: "*/*",
"content-type": "application/json",
};
if (wwbAuthorization) {
headers["wwb-provider-Authorization"] = wwbAuthorization;
}
await fetchJson(url, {
method: "PATCH",
forcePageBridge: true,
transport: "xhr",
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;
}
function closeDetailModal() {
if (appState.detailModal?.scope === "calendar-day" && appState.detailModal.value) {
appState.pendingFocusSelector = getCalendarDayFocusSelector(
appState.detailModal.value
);
}
appState.detailModal = null;
appState.timeEntriesDrawer = null;
appState.rampComparisonModal = null;
appState.forecastUpdateConfirmModal = null;
}
function shiftCalendarPeriod(delta) {
const nextDate =
appState.calendarView === "week"
? getShiftedCalendarWeekDate(delta)
: new Date(appState.calendarYear, appState.calendarMonth + delta, 1);
appState.calendarYear = nextDate.getFullYear();
appState.calendarMonth = nextDate.getMonth();
appState.calendarAnchorDateKey = getLocalDateKey(nextDate);
renderModal();
}
function goToCurrentCalendarPeriod() {
const today = new Date();
appState.calendarYear = today.getFullYear();
appState.calendarMonth = today.getMonth();
appState.calendarAnchorDateKey = getLocalDateKey(today);
renderModal();
}
function setCalendarView(view) {
const normalizedView = view === "week" ? "week" : "month";
if (appState.calendarView === normalizedView) {
return;
}
appState.calendarView = normalizedView;
setStoredCalendarView(normalizedView);
if (normalizedView === "week") {
const today = new Date();
const anchorDate =
today.getFullYear() === appState.calendarYear &&
today.getMonth() === appState.calendarMonth
? today
: new Date(appState.calendarYear, appState.calendarMonth, 1);
appState.calendarAnchorDateKey = getLocalDateKey(anchorDate);
appState.calendarYear = anchorDate.getFullYear();
appState.calendarMonth = anchorDate.getMonth();
} else {
const anchorDate = getCalendarAnchorDate(
appState.calendarYear,
appState.calendarMonth,
appState.calendarAnchorDateKey
);
appState.calendarYear = anchorDate.getFullYear();
appState.calendarMonth = anchorDate.getMonth();
}
renderModal();
}
function getShiftedCalendarWeekDate(delta) {
const anchorDate = getCalendarAnchorDate(
appState.calendarYear,
appState.calendarMonth,
appState.calendarAnchorDateKey
);
anchorDate.setDate(anchorDate.getDate() + delta * 7);
return anchorDate;
}
function shiftCalendarMonth(delta) {
shiftCalendarPeriod(delta);
}
function goToCurrentCalendarMonth() {
goToCurrentCalendarPeriod();
}
async function submitActionForm(form) {
if (!appState.dataset || !appState.actionFormModal) {
return;
}
if (!form.reportValidity()) {
return;
}
const formData = new FormData(form);
const parentId = cleanString(formData.get("parentId"));
const ownerEmail = cleanString(formData.get("ownerEmail"));
const startDate = toIsoDateString(formData.get("startDate"));
const endDate = toIsoDateString(formData.get("endDate"));
const payload = {
parentType: "WORKLOAD",
parentId,
action: {
ownerEmail,
role: "CLOUD_ARCHITECT",
name: "SUCCESS_PLAN_CONSUM_RVW",
team: cleanString(formData.get("team")),
tags: "",
startDate,
endDate,
status: "COMPLETE",
complexity: cleanString(formData.get("complexity")),
notes: [],
objective: "",
createdBy: cleanString(formData.get("createdBy")),
updatedBy: cleanString(formData.get("updatedBy")),
},
};
try {
appState.actionFormModal = {
...appState.actionFormModal,
isSubmitting: true,
errorMessage: "",
};
renderModal();
await fetchJson(API_ENDPOINTS.workloadActions, {
method: "POST",
headers: {
"content-type": "application/json; charset=UTF-8",
},
body: JSON.stringify(payload),
});
await applyCreatedActionToDataset(payload);
appState.actionFormModal = null;
renderModal();
} catch (error) {
appState.actionFormModal = {
...appState.actionFormModal,
isSubmitting: false,
errorMessage: getErrorMessage(error),
};
renderModal();
}
}
async function applyCreatedActionToDataset(payload) {
if (!appState.dataset) {
return;
}
const updatedCustomers = (appState.dataset.customers || []).map((customer) => ({
...customer,
workloads: (customer.workloads || []).map((workload) => {
if (workload.workloadId !== payload.parentId) {
return workload;
}
return {
...workload,
hasAction: true,
actions: [
...(workload.actions || []),
{
owner: payload.action.ownerEmail,
role: payload.action.role,
name: payload.action.name,
team: payload.action.team,
startDate: payload.action.startDate,
endDate: payload.action.endDate,
complexity: payload.action.complexity,
},
],
};
}),
}));
const exportPayload = createExportPayload(
appState.dataset.user,
updatedCustomers,
appState.dataset.exportPayload?.pendingServiceRequests || [],
appState.dataset.exportPayload?.resourceCurrentUser || null
);
const nextDataset = createDatasetSnapshot(exportPayload);
const previousDetailModal = appState.detailModal
? {
scope: appState.detailModal.scope,
value: appState.detailModal.value,
sortKey: appState.detailModal.sortKey,
sortDirection: appState.detailModal.sortDirection,
}
: null;
appState.dataset = nextDataset;
await writeCachedDataset(exportPayload);
if (previousDetailModal?.scope) {
restoreDetailModal(previousDetailModal);
}
}
function restoreDetailModal(detailState) {
const eligibleWorkloads = appState.dataset?.summary?.eligibleWorkloads || [];
let items = [];
let label = "";
let title = "";
if (detailState.scope === "stage") {
items = eligibleWorkloads.filter(
(workload) => workload.opportunityForecastTypeGroup === detailState.value
);
label = `Opportunity type: ${detailState.value}`;
title = `${detailState.value} workloads`;
} else if (detailState.scope === "sr") {
const flag = detailState.value === "true";
items = eligibleWorkloads.filter((workload) => workload.hasSR === flag);
label = "Status de SRs";
title = flag ? "Workloads com SR" : "Workloads sem SR";
} else if (detailState.scope === "action") {
const flag = detailState.value === "true";
items = eligibleWorkloads.filter((workload) => workload.hasAction === flag);
label = "Status de Consumption Plan/action";
title = flag ? "Workloads com action" : "Workloads sem action";
} else if (detailState.scope === "calendar-day") {
const allWorkloads = appState.dataset?.summary?.allWorkloads || [];
items = allWorkloads.filter((workload) => {
return getLocalDateKey(workload.consumptionStartDate) === detailState.value;
});
label = "Workload start calendar";
title = `Workloads starting ${formatDate(detailState.value) || detailState.value}`;
}
appState.detailModal = {
label,
title,
items,
totalAcr: items.reduce((sum, workload) => sum + workload.adjustedACR, 0),
sortKey: detailState.sortKey || DETAIL_SORT_KEYS.adjustedACR,
sortDirection: detailState.sortDirection || "desc",
scope: detailState.scope,
value: detailState.value,
type: "workloads",
};
}
function toggleDetailSort(sortKey) {
if (!appState.detailModal) {
return;
}
const nextDirection =
appState.detailModal.sortKey === sortKey &&
appState.detailModal.sortDirection === "asc"
? "desc"
: "asc";
appState.detailModal = {
...appState.detailModal,
sortKey,
sortDirection: nextDirection,
};
renderModal();
}
function getSortedDetailItems() {
if (!appState.detailModal) {
return [];
}
return sortWorkloads(
appState.detailModal.items,
appState.detailModal.sortKey,
appState.detailModal.sortDirection
);
}
function sortWorkloads(items, sortKey, sortDirection = "asc") {
const factor = sortDirection === "desc" ? -1 : 1;
return [...items].sort((left, right) => {
const comparison = compareWorkloadValues(
getSortableWorkloadValue(left, sortKey),
getSortableWorkloadValue(right, sortKey)
);
if (comparison !== 0) {
return comparison * factor;
}
return compareWorkloadValues(
getSortableWorkloadValue(left, DETAIL_SORT_KEYS.workload),
getSortableWorkloadValue(right, DETAIL_SORT_KEYS.workload)
);
});
}
function getSortableWorkloadValue(workload, sortKey) {
switch (sortKey) {
case DETAIL_SORT_KEYS.opportunityNumber:
return cleanString(workload.opportunityNumber);
case DETAIL_SORT_KEYS.customerName:
return cleanString(workload.customerName);
case DETAIL_SORT_KEYS.workload:
return `${cleanString(workload.name)}|${cleanString(workload.description)}`;
case DETAIL_SORT_KEYS.rampMonths:
return toNumber(workload.rampMonths);
case DETAIL_SORT_KEYS.adjustedACR:
return toNumber(workload.adjustedACR);
case DETAIL_SORT_KEYS.opportunityForecastTypeGroup:
return cleanString(workload.opportunityForecastTypeGroup);
case DETAIL_SORT_KEYS.hasSR:
return Boolean(workload.hasSR);
case DETAIL_SORT_KEYS.hasAction:
return Boolean(workload.hasAction);
default:
return cleanString(workload.name);
}
}
function compareWorkloadValues(left, right) {
if (typeof left === "number" || typeof right === "number") {
return toNumber(left) - toNumber(right);
}
if (typeof left === "boolean" || typeof right === "boolean") {
return Number(Boolean(left)) - Number(Boolean(right));
}
return String(left || "").localeCompare(String(right || ""), undefined, {
numeric: true,
sensitivity: "base",
});
}
function downloadFile(filename, content, contentType) {
const blob = new Blob([content], { type: contentType });
const url = URL.createObjectURL(blob);
const anchor = document.createElement("a");
anchor.href = url;
anchor.download = filename;
anchor.click();
window.setTimeout(() => {
URL.revokeObjectURL(url);
}, 1000);
}
function createExportHtml(dataset) {
const json = JSON.stringify(dataset.exportPayload);
return `<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Arch Panel Dashboard</title>
<style>
:root {
--bg: #0f141b;
--panel: #171e27;
--panel-strong: #202a36;
--border: #33404f;
--text: #f4f7fb;
--text-muted: #a8b4c2;
--accent: #7dc6e7;
--accent-soft: rgba(125, 198, 231, 0.14);
--success: #61bf81;
--warning: #ef8d67;
}
* { box-sizing: border-box; }
body {
margin: 0;
background: linear-gradient(180deg, #0d1218, #121923 45%, #10161e 100%);
color: var(--text);
font: 14px/1.45 "Segoe UI", Arial, sans-serif;
}
.shell {
max-width: 1480px;
margin: 0 auto;
padding: 28px;
}
.hero, .panel {
border: 1px solid var(--border);
background: var(--panel);
border-radius: 18px;
}
.hero {
display: flex;
justify-content: space-between;
gap: 24px;
padding: 24px;
margin-bottom: 18px;
}
.eyebrow {
margin: 0 0 8px;
color: var(--text-muted);
font-size: 11px;
font-weight: 700;
letter-spacing: .08em;
text-transform: uppercase;
}
h1, h2, h3, p { margin: 0; }
h1 { font-size: 34px; line-height: 1.1; }
.subtitle { margin-top: 12px; color: var(--text-muted); max-width: 720px; }
.pill {
display: inline-flex;
align-items: center;
min-height: 32px;
padding: 0 12px;
border-radius: 999px;
background: var(--accent-soft);
color: var(--text);
border: 1px solid rgba(255,255,255,.08);
font-weight: 600;
}
.grid {
display: grid;
gap: 16px;
}
.metrics { grid-template-columns: repeat(4, minmax(0,1fr)); margin-bottom: 18px; }
.metric, .boolean-card {
border: 1px solid var(--border);
background: var(--panel);
border-radius: 16px;
padding: 18px;
}
.metric-label { color: var(--text-muted); font-size: 12px; }
.metric-value { margin-top: 8px; font-size: 28px; font-weight: 700; }
.metric-detail { margin-top: 6px; color: var(--text-muted); font-size: 12px; }
.indicator-grid { grid-template-columns: repeat(4, minmax(0,1fr)); margin-bottom: 18px; }
.status-grid { grid-template-columns: repeat(2, minmax(0,1fr)); margin-bottom: 18px; }
.boolean-grid { display: grid; grid-template-columns: repeat(2, minmax(0,1fr)); gap: 12px; margin-top: 14px; }
.progress { display: grid; gap: 8px; margin-bottom: 12px; }
.progress-track {
display: flex;
width: 100%;
height: 10px;
overflow: hidden;
border-radius: 999px;
background: rgba(255,255,255,.06);
border: 1px solid var(--border);
}
.progress-segment { height: 100%; }
.progress-segment.is-success { background: linear-gradient(90deg, #7fd29a, #3fa35c); }
.progress-segment.is-danger { background: linear-gradient(90deg, #f2a28d, #d94f2b); }
.progress-labels {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
color: var(--text-muted);
font-size: 11px;
font-weight: 600;
}
.boolean-card {
appearance: none;
color: var(--text);
text-align: left;
cursor: pointer;
}
.boolean-card strong { display: block; margin-top: 8px; font-size: 28px; }
.layout { grid-template-columns: repeat(2, minmax(0, 1fr)); align-items: start; }
.panel { padding: 18px; }
.panel-head { display: flex; justify-content: space-between; gap: 16px; margin-bottom: 14px; }
.panel-title { margin-top: 6px; font-size: 18px; }
table { width: 100%; border-collapse: collapse; }
.main-table { table-layout: fixed; }
.main-table col.col-opportunity { width: 14%; }
.main-table col.col-customer { width: 16%; }
.main-table col.col-workload { width: auto; }
.main-table col.col-acr { width: 11%; }
.main-table col.col-type { width: 11%; }
.main-table col.col-flag { width: 9%; }
th, td { padding: 12px 14px; border-bottom: 1px solid var(--border); font-size: 12px; white-space: nowrap; }
th { color: var(--text-muted); text-align: left; background: var(--panel-strong); }
td { color: var(--text); }
td.is-numeric, th.is-numeric { text-align: right; }
.main-table th:nth-child(3),
.main-table td:nth-child(3) {
white-space: normal;
overflow-wrap: anywhere;
word-break: break-word;
}
.main-table td:nth-child(3) a {
display: block;
}
.main-table th:nth-child(2),
.main-table td:nth-child(2) {
white-space: normal;
overflow-wrap: anywhere;
}
tr:hover td { background: rgba(255,255,255,.02); }
a { color: var(--accent); text-decoration: none; }
.status {
display: inline-flex;
align-items: center;
min-height: 22px;
padding: 0 8px;
border-radius: 999px;
font-size: 11px;
font-weight: 700;
}
.status.true { color: #d9f9e4; background: rgba(97,191,129,.2); }
.status.false { color: #ffd8cc; background: rgba(239,141,103,.18); }
.row-stack { display: grid; gap: 12px; }
.row {
display: grid;
gap: 8px;
padding: 12px 0;
border-top: 1px solid var(--border);
}
.row:first-child { border-top: 0; padding-top: 0; }
.row-top {
display: flex;
justify-content: space-between;
gap: 12px;
}
.row-label { font-weight: 700; }
.row-sub { color: var(--text-muted); font-size: 12px; }
.bar { position: relative; height: 8px; border-radius: 999px; overflow: hidden; background: rgba(255,255,255,.08); }
.bar > span { position: absolute; inset: 0 auto 0 0; border-radius: inherit; background: linear-gradient(90deg, var(--accent), #4b8ce2); }
.sr-table td { white-space: normal; vertical-align: top; }
.sr-main { width: 78%; }
.sr-side { width: 22%; text-align: right; }
.sr-line { display: flex; flex-wrap: wrap; gap: 10px 14px; margin-top: 6px; }
.sr-line.head { margin-top: 0; }
.sr-customer { color: var(--text); font-size: 13px; font-weight: 700; }
.sr-moment { color: var(--text); font-size: 18px; font-weight: 700; }
.sr-exact, .sr-hours { margin-top: 6px; color: var(--text-muted); font-size: 12px; }
.empty {
padding: 18px;
border: 1px dashed var(--border);
border-radius: 8px;
color: var(--text-muted);
font-size: 13px;
}
.overlay {
position: fixed;
inset: 0;
display: none;
align-items: center;
justify-content: center;
padding: 24px;
background: rgba(8, 12, 18, .78);
backdrop-filter: blur(10px);
}
.overlay.open { display: flex; }
.detail {
width: min(1480px, calc(100vw - 32px));
max-height: calc(100vh - 32px);
overflow: auto;
border: 1px solid var(--border);
background: var(--panel);
border-radius: 18px;
padding: 18px;
}
.detail-head {
display: flex;
justify-content: space-between;
gap: 16px;
align-items: center;
margin-bottom: 12px;
}
.close {
appearance: none;
min-height: 34px;
padding: 0 14px;
border: 1px solid var(--border);
border-radius: 8px;
background: var(--panel-strong);
color: var(--text);
cursor: pointer;
}
.detail-actions {
display: flex;
justify-content: flex-end;
position: sticky;
bottom: 0;
margin-top: 16px;
padding-top: 12px;
background: linear-gradient(180deg, rgba(23, 30, 39, 0), var(--panel) 35%);
}
.detail table { table-layout: fixed; }
.detail th, .detail td {
white-space: normal;
vertical-align: top;
overflow-wrap: anywhere;
}
.detail-actions {
display: flex;
justify-content: flex-end;
margin-top: 16px;
}
.sort-button {
appearance: none;
padding: 0;
border: 0;
background: transparent;
color: inherit;
font: inherit;
font-weight: 600;
cursor: pointer;
}
.sort-button.is-numeric {
width: 100%;
text-align: right;
}
.clickable {
appearance: none;
width: 100%;
color: inherit;
text-align: left;
cursor: pointer;
}
@media (max-width: 1100px) {
.metrics, .indicator-grid, .status-grid, .layout { grid-template-columns: 1fr; }
}
</style>
</head>
<body>
<div id="app"></div>
<div id="detailOverlay" class="overlay"></div>
<script>
const payload = ${json};
const allowedTypes = ["SQL", "Pipeline", "Upside", "Forecast"];
const detailSortKeys = {
opportunityNumber: "opportunityNumber",
customerName: "customerName",
workload: "workload",
adjustedACR: "adjustedACR",
opportunityForecastTypeGroup: "opportunityForecastTypeGroup",
hasSR: "hasSR",
hasAction: "hasAction"
};
let detailState = null;
function normalizeForecastType(value) {
const text = String(value || "").trim();
return text ? text.split(" -")[0].trim() || "Unknown" : "Unknown";
}
function formatWholeNumber(value) {
return new Intl.NumberFormat("en-US", { maximumFractionDigits: 0 }).format(value);
}
function formatCurrency(value) {
return new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
maximumFractionDigits: 0
}).format(value || 0);
}
function formatPercent(value) {
return new Intl.NumberFormat("en-US", {
maximumFractionDigits: 1
}).format(value || 0) + "%";
}
function formatRelativeMoment(value) {
if (!value) {
return "-";
}
const target = new Date(value);
if (Number.isNaN(target.getTime())) {
return String(value);
}
let remaining = Math.max(Date.now() - target.getTime(), 0);
const day = 24 * 60 * 60 * 1000;
const hour = 60 * 60 * 1000;
const minute = 60 * 1000;
const second = 1000;
const days = Math.floor(remaining / day);
remaining -= days * day;
const hours = Math.floor(remaining / hour);
remaining -= hours * hour;
const minutes = Math.floor(remaining / minute);
remaining -= minutes * minute;
const seconds = Math.floor(remaining / second);
return days + "d " + hours + "h " + minutes + "m " + seconds + "s";
}
function flattenWorkloads() {
return (payload.customers || []).flatMap((customer) =>
(customer.workloads || []).map((workload) => ({
...workload,
customerName: customer.name
}))
);
}
function buildSummary(workloads) {
const eligible = workloads
.map((workload) => ({
...workload,
opportunityForecastTypeGroup: normalizeForecastType(workload.opportunityForecastTypeGroup || workload.opportunityForecastType)
}))
.filter((workload) => allowedTypes.includes(workload.opportunityForecastTypeGroup))
.sort((a, b) => (b.adjustedACR || 0) - (a.adjustedACR || 0));
return {
eligible,
stageIndicators: allowedTypes.map((label) => {
const items = eligible.filter((workload) => workload.opportunityForecastTypeGroup === label);
return {
label,
count: items.length,
totalAcr: items.reduce((sum, item) => sum + (item.adjustedACR || 0), 0)
};
}),
sr: {
trueCount: eligible.filter((item) => item.hasSR).length,
falseCount: eligible.filter((item) => !item.hasSR).length
},
action: {
trueCount: eligible.filter((item) => item.hasAction).length,
falseCount: eligible.filter((item) => !item.hasAction).length
}
};
}
function opportunityLink(workload) {
if (!workload.opportunityId || !workload.opportunityNumber) {
return workload.opportunityNumber || "Opportunity";
}
const href = "https://eeho.fa.us2.oraclecloud.com/fscmUI/redwood/cx-sales/application/container/opportunities/opportunities-detail?id=" +
encodeURIComponent(workload.opportunityId) +
"&puid=" +
encodeURIComponent(workload.opportunityNumber);
return '<a href="' + href + '" target="_blank" rel="noreferrer">' + workload.opportunityNumber + '</a>';
}
function workloadLink(workload) {
const label = (workload.name || "Unnamed workload") + (workload.description ? " | " + workload.description : "");
if (!workload.workloadId) {
return label;
}
const href = "https://spa.oracle.com/oalcrm/web/api/g2m-consumer-application/ui/index.html?ojr=workload_workbench/workload_details;workloadId=" +
encodeURIComponent(workload.workloadId);
return '<a href="' + href + '" target="_blank" rel="noreferrer">' + label + '</a>';
}
function statusToken(value) {
const label = value === "true" ? "Sim" : "Não";
return '<span class="status ' + value + '">' + label + '</span>';
}
function renderTable(items) {
return '<table class="main-table"><colgroup><col class="col-opportunity"><col class="col-customer"><col class="col-workload"><col class="col-acr"><col class="col-type"><col class="col-flag"><col class="col-flag"></colgroup><thead><tr><th>Opportunity</th><th>Customer</th><th>Workload</th><th class="is-numeric">ACR</th><th>Type</th><th>Tem SR?</th><th>Tem Action?</th></tr></thead><tbody>' +
items.map((workload) => '<tr><td>' + opportunityLink(workload) + '</td><td>' + (workload.customerName || '') + '</td><td>' + workloadLink(workload) + '</td><td class="is-numeric">' + formatCurrency(workload.adjustedACR) + '</td><td>' + workload.opportunityForecastTypeGroup + '</td><td>' + statusToken(String(Boolean(workload.hasSR))) + '</td><td>' + statusToken(String(Boolean(workload.hasAction))) + '</td></tr>').join('') +
'</tbody></table>';
}
function getSortableValue(workload, sortKey) {
if (sortKey === detailSortKeys.opportunityNumber) {
return String(workload.opportunityNumber || "");
}
if (sortKey === detailSortKeys.customerName) {
return String(workload.customerName || "");
}
if (sortKey === detailSortKeys.workload) {
return String((workload.name || "") + "|" + (workload.description || ""));
}
if (sortKey === detailSortKeys.adjustedACR) {
return Number(workload.adjustedACR || 0);
}
if (sortKey === detailSortKeys.opportunityForecastTypeGroup) {
return String(workload.opportunityForecastTypeGroup || "");
}
if (sortKey === detailSortKeys.hasSR) {
return Boolean(workload.hasSR);
}
if (sortKey === detailSortKeys.hasAction) {
return Boolean(workload.hasAction);
}
return String(workload.name || "");
}
function compareSortableValues(left, right) {
if (typeof left === "number" || typeof right === "number") {
return Number(left || 0) - Number(right || 0);
}
if (typeof left === "boolean" || typeof right === "boolean") {
return Number(Boolean(left)) - Number(Boolean(right));
}
return String(left || "").localeCompare(String(right || ""), undefined, {
numeric: true,
sensitivity: "base"
});
}
function sortItems(items, sortKey, sortDirection) {
const factor = sortDirection === "desc" ? -1 : 1;
return items.slice().sort((left, right) => {
const comparison = compareSortableValues(
getSortableValue(left, sortKey),
getSortableValue(right, sortKey)
);
if (comparison !== 0) {
return comparison * factor;
}
return compareSortableValues(
getSortableValue(left, detailSortKeys.workload),
getSortableValue(right, detailSortKeys.workload)
);
});
}
function renderSortButton(label, sortKey, isNumeric) {
const direction = detailState && detailState.sortKey === sortKey
? (detailState.sortDirection === "asc" ? " ↑" : " ↓")
: "";
return '<button class="sort-button' + (isNumeric ? ' is-numeric' : '') + '" data-sort-key="' + sortKey + '">' + label + direction + '</button>';
}
function renderDetailTable(items) {
return '<table><thead><tr><th>' + renderSortButton('Opportunity', detailSortKeys.opportunityNumber, false) + '</th><th>' + renderSortButton('Customer', detailSortKeys.customerName, false) + '</th><th>' + renderSortButton('Workload', detailSortKeys.workload, false) + '</th><th class="is-numeric">' + renderSortButton('ACR', detailSortKeys.adjustedACR, true) + '</th><th>' + renderSortButton('Type', detailSortKeys.opportunityForecastTypeGroup, false) + '</th><th>' + renderSortButton('HasSR', detailSortKeys.hasSR, false) + '</th><th>' + renderSortButton('HasAction', detailSortKeys.hasAction, false) + '</th></tr></thead><tbody>' +
items.map((workload) => '<tr><td>' + opportunityLink(workload) + '</td><td>' + (workload.customerName || '') + '</td><td>' + workloadLink(workload) + '</td><td class="is-numeric">' + formatCurrency(workload.adjustedACR) + '</td><td>' + workload.opportunityForecastTypeGroup + '</td><td>' + statusToken(String(Boolean(workload.hasSR))) + '</td><td>' + statusToken(String(Boolean(workload.hasAction))) + '</td></tr>').join('') +
'</tbody></table>';
}
function openDetail(title, label, items) {
const totalAcr = items.reduce((sum, item) => sum + (item.adjustedACR || 0), 0);
const overlay = document.getElementById("detailOverlay");
overlay.innerHTML = '<div class="detail"><div class="detail-head"><div><p class="eyebrow">' + label + '</p><h2>' + title + '</h2></div><button class="close" id="closeDetail">Close</button></div><p class="subtitle">' + formatWholeNumber(items.length) + ' workloads · Total (ACR): ' + formatCurrency(totalAcr) + '</p><div style="margin-top:16px">' + renderTable(items) + '</div></div>';
overlay.classList.add("open");
document.getElementById("closeDetail").addEventListener("click", () => overlay.classList.remove("open"));
overlay.onclick = (event) => {
if (event.target === overlay) {
overlay.classList.remove("open");
}
};
}
function renderSortButtonStateful(label, sortKey, isNumeric) {
const direction = detailState && detailState.sortKey === sortKey
? (detailState.sortDirection === "asc" ? " ^" : " v")
: "";
return '<button class="sort-button' + (isNumeric ? ' is-numeric' : '') + '" data-sort-key="' + sortKey + '">' + label + direction + '</button>';
}
function renderDetailTableStateful(items) {
return '<table><thead><tr><th>' + renderSortButtonStateful('Opportunity', detailSortKeys.opportunityNumber, false) + '</th><th>' + renderSortButtonStateful('Customer', detailSortKeys.customerName, false) + '</th><th>' + renderSortButtonStateful('Workload', detailSortKeys.workload, false) + '</th><th class="is-numeric">' + renderSortButtonStateful('ACR', detailSortKeys.adjustedACR, true) + '</th><th>' + renderSortButtonStateful('Type', detailSortKeys.opportunityForecastTypeGroup, false) + '</th><th>' + renderSortButtonStateful('Tem SR?', detailSortKeys.hasSR, false) + '</th><th>' + renderSortButtonStateful('Tem Action?', detailSortKeys.hasAction, false) + '</th></tr></thead><tbody>' +
items.map((workload) => '<tr><td>' + opportunityLink(workload) + '</td><td>' + (workload.customerName || '') + '</td><td>' + workloadLink(workload) + '</td><td class="is-numeric">' + formatCurrency(workload.adjustedACR) + '</td><td>' + workload.opportunityForecastTypeGroup + '</td><td>' + statusToken(String(Boolean(workload.hasSR))) + '</td><td>' + statusToken(String(Boolean(workload.hasAction))) + '</td></tr>').join('') +
'</tbody></table>';
}
function openDetailStateful(title, label, items) {
detailState = {
title: title,
label: label,
items: items.slice(),
sortKey: detailSortKeys.adjustedACR,
sortDirection: "desc"
};
renderDetailStateful();
}
function renderDetailStateful() {
if (!detailState) {
return;
}
const totalAcr = detailState.items.reduce((sum, item) => sum + (item.adjustedACR || 0), 0);
const sortedItems = sortItems(detailState.items, detailState.sortKey, detailState.sortDirection);
const overlay = document.getElementById("detailOverlay");
overlay.innerHTML = '<div class="detail"><div class="detail-head"><div><p class="eyebrow">' + detailState.label + '</p><h2>' + detailState.title + '</h2></div><button class="close" id="closeDetail">Fechar</button></div><p class="subtitle">' + formatWholeNumber(sortedItems.length) + ' workloads · Total (ACR): ' + formatCurrency(totalAcr) + '</p><div style="margin-top:16px">' + renderDetailTableStateful(sortedItems) + '</div><div class="detail-actions"><button class="close" id="closeDetailFooter">Fechar</button></div></div>';
overlay.classList.add("open");
const close = () => {
overlay.classList.remove("open");
detailState = null;
};
document.getElementById("closeDetail").addEventListener("click", close);
document.getElementById("closeDetailFooter").addEventListener("click", close);
overlay.querySelectorAll("[data-sort-key]").forEach((button) => {
button.addEventListener("click", () => {
const sortKey = button.getAttribute("data-sort-key");
const nextDirection = detailState.sortKey === sortKey && detailState.sortDirection === "asc"
? "desc"
: "asc";
detailState.sortKey = sortKey;
detailState.sortDirection = nextDirection;
renderDetailStateful();
});
});
overlay.onclick = (event) => {
if (event.target === overlay) {
close();
}
};
}
function render() {
const workloads = flattenWorkloads();
const summary = buildSummary(workloads);
const app = document.getElementById("app");
app.innerHTML = '<div class="shell">' +
'<section class="hero"><div><p class="eyebrow">Live Oracle dataset</p><h1>Arch panel export</h1><p class="subtitle">Generated from user, customer, workload, action and service request APIs.</p></div><div><span class="pill">' + formatCurrency(summary.eligible.reduce((sum, item) => sum + (item.adjustedACR || 0), 0)) + ' total eligible ACR</span></div></section>' +
'<section class="grid metrics">' +
'<article class="metric"><p class="metric-label">Total clients</p><p class="metric-value">' + formatWholeNumber(payload.customers.length) + '</p><p class="metric-detail">Loaded from listSummary</p></article>' +
'<article class="metric"><p class="metric-label">Total workloads</p><p class="metric-value">' + formatWholeNumber(workloads.length) + '</p><p class="metric-detail">Won workloads excluded upstream</p></article>' +
'<article class="metric"><p class="metric-label">Current user</p><p class="metric-value" style="font-size:18px">' + (payload.user.name || payload.user.userEmail) + '</p><p class="metric-detail">' + payload.user.userEmail + '</p></article>' +
'</section>' +
'<section class="grid indicator-grid">' +
summary.stageIndicators.map((indicator) => '<button class="metric clickable" data-kind="stage" data-value="' + indicator.label + '"><p class="metric-label">' + indicator.label + '</p><p class="metric-value">' + formatWholeNumber(indicator.count) + '</p><p class="metric-detail">Total (ACR): ' + formatCurrency(indicator.totalAcr) + '</p></button>').join('') +
'</section>' +
'<section class="grid status-grid">' +
'<article class="panel"><p class="eyebrow">Status de SRs</p><div class="progress"><div class="progress-track"><span class="progress-segment is-success" style="width:' + ((summary.sr.trueCount + summary.sr.falseCount) > 0 ? (summary.sr.trueCount / (summary.sr.trueCount + summary.sr.falseCount)) * 100 : 0) + '%"></span><span class="progress-segment is-danger" style="width:' + ((summary.sr.trueCount + summary.sr.falseCount) > 0 ? (summary.sr.falseCount / (summary.sr.trueCount + summary.sr.falseCount)) * 100 : 0) + '%"></span></div><div class="progress-labels"><span>Workloads com SR: ' + formatPercent((summary.sr.trueCount + summary.sr.falseCount) > 0 ? (summary.sr.trueCount / (summary.sr.trueCount + summary.sr.falseCount)) * 100 : 0) + '</span><span>Workloads sem SR: ' + formatPercent((summary.sr.trueCount + summary.sr.falseCount) > 0 ? (summary.sr.falseCount / (summary.sr.trueCount + summary.sr.falseCount)) * 100 : 0) + '</span></div></div><div class="boolean-grid"><button class="boolean-card" data-kind="sr" data-value="true"><span>Workloads com SR</span><strong>' + formatWholeNumber(summary.sr.trueCount) + '</strong></button><button class="boolean-card" data-kind="sr" data-value="false"><span>Workloads sem SR</span><strong>' + formatWholeNumber(summary.sr.falseCount) + '</strong></button></div></article>' +
'<article class="panel"><p class="eyebrow">Status de Consumption Plan/action</p><div class="progress"><div class="progress-track"><span class="progress-segment is-success" style="width:' + ((summary.action.trueCount + summary.action.falseCount) > 0 ? (summary.action.trueCount / (summary.action.trueCount + summary.action.falseCount)) * 100 : 0) + '%"></span><span class="progress-segment is-danger" style="width:' + ((summary.action.trueCount + summary.action.falseCount) > 0 ? (summary.action.falseCount / (summary.action.trueCount + summary.action.falseCount)) * 100 : 0) + '%"></span></div><div class="progress-labels"><span>Workloads com action: ' + formatPercent((summary.action.trueCount + summary.action.falseCount) > 0 ? (summary.action.trueCount / (summary.action.trueCount + summary.action.falseCount)) * 100 : 0) + '</span><span>Workloads sem action: ' + formatPercent((summary.action.trueCount + summary.action.falseCount) > 0 ? (summary.action.falseCount / (summary.action.trueCount + summary.action.falseCount)) * 100 : 0) + '</span></div></div><div class="boolean-grid"><button class="boolean-card" data-kind="action" data-value="true"><span>Workloads com action</span><strong>' + formatWholeNumber(summary.action.trueCount) + '</strong></button><button class="boolean-card" data-kind="action" data-value="false"><span>Workloads sem action</span><strong>' + formatWholeNumber(summary.action.falseCount) + '</strong></button></div></article>' +
'</section>' +
'</div>';
app.querySelectorAll("[data-kind]").forEach((element) => {
element.addEventListener("click", () => {
const kind = element.getAttribute("data-kind");
const value = element.getAttribute("data-value");
let items = [];
let title = "";
let label = "";
if (kind === "stage") {
items = summary.eligible.filter((item) => item.opportunityForecastTypeGroup === value);
label = "Opportunity type: " + value;
title = value + " workloads";
} else if (kind === "sr") {
const flag = value === "true";
items = summary.eligible.filter((item) => Boolean(item.hasSR) === flag);
label = "Status de SRs";
title = flag ? "Workloads com SR" : "Workloads sem SR";
} else if (kind === "action") {
const flag = value === "true";
items = summary.eligible.filter((item) => Boolean(item.hasAction) === flag);
label = "Status de Consumption Plan/action";
title = flag ? "Workloads com action" : "Workloads sem action";
}
openDetailStateful(title, label, items);
});
});
}
render();
</script>
</body>
</html>`;
}
function toggleTheme() {
appState.theme = appState.theme === "dark" ? "light" : "dark";
setStoredTheme(appState.theme);
renderModal();
}
function toggleSidebar() {
appState.sidebarCollapsed = !appState.sidebarCollapsed;
setStoredSidebarCollapsed(appState.sidebarCollapsed);
renderModal();
}
function applyDialogTheme(overlay) {
const dialog = overlay?.querySelector(`#${IDS.dialog}`);
if (!dialog) {
return;
}
dialog.setAttribute("data-theme", appState.theme);
}
function ensureStyles() {
if (document.getElementById(IDS.style)) {
return;
}
const style = document.createElement("style");
style.id = IDS.style;
style.textContent = `
html.${CLASSES.modalOpen},
body.${CLASSES.modalOpen} {
overflow: hidden !important;
}
.arch-panel-extension-entry {
--arch-panel-bg: rgba(255, 255, 255, 0.12);
--arch-panel-bg-hover: rgba(255, 255, 255, 0.2);
--arch-panel-border: rgba(255, 255, 255, 0.26);
--arch-panel-fg: #ffffff;
--arch-panel-focus: rgba(255, 255, 255, 0.72);
--arch-panel-font: inherit;
--arch-workbench-header: #4b335d;
--arch-workbench-accent: #6c4d80;
display: inline-flex;
align-items: center;
margin-inline-end: 12px;
flex: 0 0 auto;
}
.arch-panel-extension-button {
appearance: none;
border: 1px solid var(--arch-panel-border);
background: var(--arch-panel-bg);
color: var(--arch-panel-fg);
border-radius: 999px;
min-height: 36px;
padding: 0 14px;
display: inline-flex;
align-items: center;
justify-content: center;
font-family: var(--arch-panel-font);
font-size: 13px;
font-weight: 600;
line-height: 1;
letter-spacing: 0.01em;
cursor: pointer;
transition:
background-color 160ms ease,
border-color 160ms ease,
transform 160ms ease;
}
.arch-panel-extension-button:hover {
background: var(--arch-panel-bg-hover);
}
.arch-panel-extension-button:active {
transform: translateY(1px);
}
.arch-panel-extension-button:focus-visible,
.arch-panel-extension-close:focus-visible,
.arch-panel-extension-theme-toggle:focus-visible,
.arch-panel-extension-sidebar-button:focus-visible,
.arch-panel-extension-utility-button:focus-visible,
.arch-panel-extension-summary-card-button:focus-visible,
.arch-panel-extension-sort-button:focus-visible,
.arch-panel-extension-indicator-card:focus-visible,
.arch-panel-extension-row-link:focus-visible {
outline: 2px solid var(--arch-panel-focus);
outline-offset: 2px;
}
.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);
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-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: var(--arch-workbench-header);
color: #ffffff;
}
.arch-panel-extension-brand {
display: flex;
align-items: center;
gap: 14px;
}
.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.14);
color: #ffffff;
font-size: 14px;
font-weight: 700;
}
.arch-panel-extension-brand-label {
margin: 0 0 2px;
color: rgba(255, 255, 255, 0.72);
font-size: 11px;
letter-spacing: 0.08em;
text-transform: uppercase;
}
.arch-panel-extension-title {
margin: 0;
color: #ffffff;
font-size: 24px;
line-height: 1.2;
font-weight: 600;
}
.arch-panel-extension-shell-actions {
display: flex;
align-items: center;
gap: 12px;
}
.arch-panel-extension-shell-note {
color: rgba(255, 255, 255, 0.76);
font-size: 12px;
}
.arch-panel-extension-theme-toggle {
appearance: none;
min-height: 34px;
padding: 0 14px;
border: 1px solid rgba(255, 255, 255, 0.24);
border-radius: 4px;
background: rgba(255, 255, 255, 0.08);
color: #ffffff;
font: inherit;
font-size: 13px;
font-weight: 600;
cursor: pointer;
}
.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.24);
background: rgba(255, 255, 255, 0.08);
color: #ffffff;
}
.arch-panel-extension-shell-header .arch-panel-extension-close:hover {
border-color: rgba(255, 255, 255, 0.42);
background: rgba(255, 255, 255, 0.16);
}
.arch-panel-extension-detail-dialog .arch-panel-extension-close {
border-color: var(--wb-border);
background: var(--wb-panel-soft);
color: var(--wb-text);
box-shadow: none;
}
.arch-panel-extension-sidebar-button,
.arch-panel-extension-utility-button {
appearance: none;
min-height: 34px;
padding: 0 14px;
border: 1px solid var(--wb-border);
border-radius: 4px;
background: var(--wb-panel);
color: var(--wb-text);
font: inherit;
font-size: 13px;
font-weight: 600;
cursor: pointer;
}
.arch-panel-extension-utility-button.is-primary {
border-color: #0b5cab;
background: #0b5cab;
color: #ffffff;
text-shadow: 0 1px 0 rgba(0, 0, 0, 0.16);
box-shadow: 0 8px 18px rgba(11, 92, 171, 0.24);
}
.arch-panel-extension-utility-button.is-primary:hover {
border-color: #084a8a;
background: #084a8a;
}
.arch-panel-extension-utility-button.is-primary:disabled {
border-color: var(--wb-border);
background: var(--wb-panel-soft);
color: var(--wb-text-muted);
box-shadow: none;
cursor: not-allowed;
}
.arch-panel-extension-shell-body {
display: grid;
grid-template-columns: 260px minmax(0, 1fr);
min-height: 0;
position: relative;
}
.arch-panel-extension-shell-body.is-sidebar-collapsed {
grid-template-columns: minmax(0, 1fr);
}
.arch-panel-extension-sidebar {
background: var(--wb-sidebar);
border-right: 1px solid var(--wb-border);
padding: 20px 14px;
}
.arch-panel-extension-sidebar.is-collapsed {
display: none;
}
.arch-panel-extension-sidebar-group + .arch-panel-extension-sidebar-group {
margin-top: 18px;
padding-top: 18px;
border-top: 1px solid var(--wb-border);
}
.arch-panel-extension-sidebar-label,
.arch-panel-extension-page-eyebrow,
.arch-panel-extension-panel-label,
.arch-panel-extension-state-eyebrow {
margin: 0 0 8px;
color: var(--wb-text-muted);
font-size: 11px;
font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
}
.arch-panel-extension-sidebar-data {
display: grid;
gap: 6px;
color: var(--wb-text-secondary);
font-size: 12px;
}
.arch-panel-extension-sidebar-data strong {
color: var(--wb-text);
font-size: 13px;
}
.arch-panel-extension-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-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;
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-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-error {
margin: 12px 0 0;
color: #d94f2b;
font-size: 12px;
font-weight: 600;
}
.arch-panel-extension-sort-button {
appearance: none;
display: inline-flex;
align-items: center;
gap: 4px;
padding: 0;
border: 0;
background: transparent;
color: inherit;
font: inherit;
font-weight: 600;
cursor: pointer;
}
.arch-panel-extension-sort-button.is-numeric {
justify-content: flex-end;
width: 100%;
}
.arch-panel-extension-table-wrap.is-detail {
flex: 1 1 auto;
min-height: 0;
max-height: calc(100vh - 270px);
overflow: auto;
}
.arch-panel-extension-table-wrap.is-detail .arch-panel-extension-table th {
position: sticky;
top: 0;
z-index: 3;
}
.arch-panel-extension-table-wrap.is-detail .arch-panel-extension-table thead {
position: relative;
z-index: 3;
}
.arch-panel-extension-table-wrap.is-detail .arch-panel-extension-table {
border-collapse: separate;
border-spacing: 0;
}
.arch-panel-extension-table-wrap.is-detail .arch-panel-extension-table {
table-layout: fixed;
}
.arch-panel-extension-table-wrap.is-detail .arch-panel-extension-table th,
.arch-panel-extension-table-wrap.is-detail .arch-panel-extension-table td {
white-space: normal;
vertical-align: top;
overflow-wrap: anywhere;
}
.arch-panel-extension-state-actions {
display: flex;
gap: 12px;
margin-top: 18px;
}
.arch-panel-extension-state-card.is-error {
border-color: rgba(217, 79, 43, 0.42);
}
@media (max-width: 1280px) {
.arch-panel-extension-summary-row,
.arch-panel-extension-indicator-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
@media (max-width: 1100px) {
.arch-panel-extension-shell-body,
.arch-panel-extension-content-grid,
.arch-panel-extension-status-grid,
.arch-panel-extension-operations-grid {
grid-template-columns: 1fr;
}
.arch-panel-extension-sidebar {
display: none;
}
}
@media (max-width: 780px) {
.arch-panel-extension-shell-header,
.arch-panel-extension-page-hero {
flex-direction: column;
align-items: flex-start;
}
.arch-panel-extension-summary-row,
.arch-panel-extension-indicator-grid,
.arch-panel-extension-boolean-grid,
.arch-panel-extension-sr-hour-grid {
grid-template-columns: 1fr;
}
.arch-panel-extension-state-card {
min-height: 260px;
padding: 26px 24px;
}
}
@media (max-width: 980px) {
.arch-panel-extension-action-dialog {
width: min(720px, calc(100vw - 24px));
}
.arch-panel-extension-form-grid {
grid-template-columns: 1fr;
}
}
`;
document.head.appendChild(style);
}
function applyTheme(header, functionsContainer, anchor, mount, overlay) {
const headerStyle = window.getComputedStyle(header);
const functionsStyle = window.getComputedStyle(functionsContainer);
const sourceBackground = resolveBackgroundColor(header, headerStyle);
const sourceForeground = resolveForegroundColor(
header,
functionsContainer,
anchor,
headerStyle,
functionsStyle
);
const workbenchHeader = mix(sourceBackground, { r: 75, g: 51, b: 93, a: 1 }, 0.52);
const workbenchAccent = mix(workbenchHeader, { r: 108, g: 77, b: 128, a: 1 }, 0.38);
const focusColor = mix(sourceForeground, workbenchHeader, 0.28);
const themeTargets = [mount, overlay].filter(Boolean);
for (const target of themeTargets) {
target.style.setProperty(
"--arch-panel-bg",
toColor(mix(sourceBackground, sourceForeground, 0.12))
);
target.style.setProperty(
"--arch-panel-bg-hover",
toColor(mix(sourceBackground, sourceForeground, 0.22))
);
target.style.setProperty(
"--arch-panel-border",
toColor(mix(sourceBackground, sourceForeground, 0.32))
);
target.style.setProperty("--arch-panel-fg", toColor(sourceForeground, 1));
target.style.setProperty("--arch-panel-focus", toColor(focusColor, 0.94));
target.style.setProperty(
"--arch-panel-font",
functionsStyle.fontFamily || headerStyle.fontFamily || "inherit"
);
target.style.setProperty("--arch-workbench-header", toColor(workbenchHeader, 1));
target.style.setProperty("--arch-workbench-accent", toColor(workbenchAccent, 1));
}
}
function resolveForegroundColor(
header,
functionsContainer,
anchor,
headerStyle,
functionsStyle
) {
const salesPlanningElement = findTextElement(header, "Sales Planning");
const salesPlanningColor = salesPlanningElement
? parseColor(window.getComputedStyle(salesPlanningElement).color)
: null;
const functionsItemColor = findVisibleChildColor(functionsContainer);
const userColor = anchor
? parseColor(window.getComputedStyle(anchor).color)
: null;
return (
userColor ||
functionsItemColor ||
parseColor(functionsStyle.color) ||
salesPlanningColor ||
parseColor(headerStyle.color) || {
r: 255,
g: 255,
b: 255,
a: 1,
}
);
}
function resolveBackgroundColor(element, style) {
const directBackground = parseColor(style.backgroundColor);
if (directBackground && directBackground.a > 0) {
return directBackground;
}
let current = element.parentElement;
while (current) {
const currentBackground = parseColor(
window.getComputedStyle(current).backgroundColor
);
if (currentBackground && currentBackground.a > 0) {
return currentBackground;
}
current = current.parentElement;
}
return { r: 75, g: 51, b: 93, a: 1 };
}
function parseColor(value) {
if (!value || value === "transparent") {
return null;
}
const rgbMatch = value.match(
/rgba?\(\s*(\d{1,3})[\s,]+(\d{1,3})[\s,]+(\d{1,3})(?:[\s,/]+([.\d]+))?\s*\)/i
);
if (rgbMatch) {
return {
r: Number.parseInt(rgbMatch[1], 10),
g: Number.parseInt(rgbMatch[2], 10),
b: Number.parseInt(rgbMatch[3], 10),
a: rgbMatch[4] === undefined ? 1 : Number.parseFloat(rgbMatch[4]),
};
}
const hexMatch = value.match(/^#([\da-f]{3,8})$/i);
if (!hexMatch) {
return null;
}
const hexValue = hexMatch[1];
if (hexValue.length === 3 || hexValue.length === 4) {
const [r, g, b, a = "f"] = hexValue.split("");
return {
r: Number.parseInt(r + r, 16),
g: Number.parseInt(g + g, 16),
b: Number.parseInt(b + b, 16),
a: Number.parseInt(a + a, 16) / 255,
};
}
if (hexValue.length === 6 || hexValue.length === 8) {
return {
r: Number.parseInt(hexValue.slice(0, 2), 16),
g: Number.parseInt(hexValue.slice(2, 4), 16),
b: Number.parseInt(hexValue.slice(4, 6), 16),
a:
hexValue.length === 8
? Number.parseInt(hexValue.slice(6, 8), 16) / 255
: 1,
};
}
return null;
}
function mix(base, tint, amount) {
const ratio = clamp(amount, 0, 1);
return {
r: Math.round(base.r + (tint.r - base.r) * ratio),
g: Math.round(base.g + (tint.g - base.g) * ratio),
b: Math.round(base.b + (tint.b - base.b) * ratio),
a: base.a + (tint.a - base.a) * ratio,
};
}
function toColor(color, alphaOverride) {
const alpha = alphaOverride ?? color.a ?? 1;
return `rgba(${color.r}, ${color.g}, ${color.b}, ${clamp(alpha, 0, 1)})`;
}
function clamp(value, min, max) {
return Math.min(Math.max(value, min), max);
}
function findUserAnchor(container) {
const selector = [
`button[label="${USER_BUTTON_LABEL}"]`,
`[role="button"][label="${USER_BUTTON_LABEL}"]`,
`oj-button[label="${USER_BUTTON_LABEL}"]`,
`button[aria-label="${USER_BUTTON_LABEL}"]`,
`[role="button"][aria-label="${USER_BUTTON_LABEL}"]`,
`[label="${USER_BUTTON_LABEL}"]`,
`[aria-label="${USER_BUTTON_LABEL}"]`,
].join(", ");
const directMatch = container.querySelector(selector);
if (directMatch && isVisible(directMatch)) {
return directMatch;
}
const fallbackMatch = Array.from(container.querySelectorAll("*")).find(
(element) => {
if (!isVisible(element) || element.closest(`#${IDS.mount}`)) {
return false;
}
return (
element.getAttribute("label") === USER_BUTTON_LABEL ||
element.getAttribute("aria-label") === USER_BUTTON_LABEL
);
}
);
return fallbackMatch || null;
}
function findTextElement(container, expectedText) {
const normalizedExpected = normalizeText(expectedText);
const elements = Array.from(container.querySelectorAll("*"));
return (
elements.find((element) => {
const text = normalizeText(element.textContent || "");
return text && text.includes(normalizedExpected);
}) || null
);
}
function resolveInsertionTarget(container, anchor, mount) {
const directAnchor = anchor ? getDirectChild(anchor, container) : null;
const fallbackTarget = findFirstInsertionTarget(container, mount);
const candidate = directAnchor || fallbackTarget;
if (!candidate) {
return null;
}
if (candidate === mount) {
return findNextValidSibling(container, mount);
}
return candidate.parentElement === container ? candidate : null;
}
function getDirectChild(element, container) {
let current = element;
while (current && current.parentElement !== container) {
current = current.parentElement;
}
return current && current.parentElement === container ? current : null;
}
function findFirstInsertionTarget(container, mount) {
return Array.from(container.children).find((child) => child !== mount) || null;
}
function findNextValidSibling(container, mount) {
let sibling = mount.nextElementSibling;
while (sibling) {
if (sibling.parentElement === container) {
return sibling;
}
sibling = sibling.nextElementSibling;
}
return null;
}
function findVisibleChildColor(container) {
const visibleChild = Array.from(container.querySelectorAll("*")).find(
(element) => isVisible(element) && !element.closest(`#${IDS.mount}`)
);
return visibleChild
? parseColor(window.getComputedStyle(visibleChild).color)
: null;
}
function extractArray(value, candidateKeys = []) {
if (Array.isArray(value)) {
return value;
}
if (!value || typeof value !== "object") {
return [];
}
for (const key of candidateKeys) {
if (Array.isArray(value[key])) {
return value[key];
}
}
for (const nestedValue of Object.values(value)) {
if (Array.isArray(nestedValue)) {
return nestedValue;
}
if (nestedValue && typeof nestedValue === "object") {
const nestedArray = extractArray(nestedValue, candidateKeys);
if (nestedArray.length > 0) {
return nestedArray;
}
}
}
return [];
}
async function mapWithConcurrency(items, concurrency, mapper) {
const results = new Array(items.length);
let nextIndex = 0;
async function worker() {
while (nextIndex < items.length) {
const currentIndex = nextIndex;
nextIndex += 1;
results[currentIndex] = await mapper(items[currentIndex], currentIndex);
}
}
const workerCount = Math.min(concurrency, items.length || 1);
await Promise.all(Array.from({ length: workerCount }, () => worker()));
return results;
}
function getStoredTheme() {
try {
const storedTheme = window.localStorage.getItem(STORAGE_KEYS.theme);
return storedTheme === "dark" ? "dark" : "light";
} catch {
return "light";
}
}
function setStoredTheme(theme) {
try {
window.localStorage.setItem(STORAGE_KEYS.theme, theme);
} catch {
// Ignore storage issues and keep the in-memory theme.
}
}
function getStoredSidebarCollapsed() {
try {
return window.localStorage.getItem(STORAGE_KEYS.sidebarCollapsed) === "true";
} catch {
return false;
}
}
function setStoredSidebarCollapsed(value) {
try {
window.localStorage.setItem(STORAGE_KEYS.sidebarCollapsed, String(Boolean(value)));
} catch {
// Ignore storage issues and keep the in-memory state.
}
}
function getStoredCalendarView() {
try {
return window.localStorage.getItem(STORAGE_KEYS.calendarView) === "week"
? "week"
: "month";
} catch {
return "month";
}
}
function setStoredCalendarView(value) {
try {
window.localStorage.setItem(
STORAGE_KEYS.calendarView,
value === "week" ? "week" : "month"
);
} catch {
// Ignore storage issues and keep the in-memory calendar view.
}
}
function normalizeText(value) {
return String(value || "").replace(/\s+/g, " ").trim();
}
function cleanString(value) {
return normalizeText(value);
}
function firstNonEmptyString(values) {
for (const value of values) {
const normalized = cleanString(value);
if (normalized) {
return normalized;
}
}
return "";
}
function extractAuthHeaders(tokenPayload) {
if (!tokenPayload) {
return {};
}
if (typeof tokenPayload === "string") {
return {
authorization: `Bearer ${tokenPayload}`,
};
}
const tokenSources = [
tokenPayload,
tokenPayload?.token,
tokenPayload?.tokens,
tokenPayload?.oauth,
tokenPayload?.oauthToken,
tokenPayload?.authentication,
tokenPayload?.auth,
].filter(Boolean);
const headers = {};
const explicitHeaderSources = [
tokenPayload?.headers,
tokenPayload?.requestHeaders,
tokenPayload?.authHeaders,
].filter((value) => value && typeof value === "object");
for (const explicitHeaderSource of explicitHeaderSources) {
for (const [key, value] of Object.entries(explicitHeaderSource)) {
const normalizedKey = cleanString(key).toLowerCase();
const normalizedValue = cleanString(value);
if (!normalizedKey || !normalizedValue) {
continue;
}
headers[normalizedKey] = normalizedValue;
}
}
for (const tokenSource of tokenSources) {
const tokenType =
firstNonEmptyString([
tokenSource?.tokenType,
tokenSource?.type,
tokenSource?.token_type,
]) || "Bearer";
const accessToken = firstNonEmptyString([
tokenSource?.accessToken,
tokenSource?.access_token,
tokenSource?.token,
tokenSource?.jwt,
]);
const idToken = firstNonEmptyString([
tokenSource?.idToken,
tokenSource?.id_token,
]);
if (accessToken && !headers.authorization) {
headers.authorization = `${tokenType} ${accessToken}`;
}
if (idToken && !headers["x-id-token"]) {
headers["x-id-token"] = idToken;
}
}
return headers;
}
function normalizeString(value) {
return normalizeText(value).toUpperCase();
}
function toBridgeRequestUrl(url) {
const normalized = cleanString(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 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 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 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 getTimeEntryField(item, keys) {
const directValue = getFieldValue(item, keys);
if (directValue !== undefined && directValue !== null && directValue !== "") {
return directValue;
}
const normalizedKeys = keys.map((key) => key.toLowerCase().replace(/[^a-z0-9]/g, ""));
const flattenedMatch = flattenDisplayRecord(item).find(([key, value]) => {
const normalizedKey = key.toLowerCase().replace(/[^a-z0-9]/g, "");
return normalizedKeys.includes(normalizedKey) && cleanString(value);
});
return flattenedMatch ? flattenedMatch[1] : "";
}
function flattenDisplayRecord(value, prefix = "", output = []) {
if (!value || typeof value !== "object" || Array.isArray(value)) {
if (prefix) {
output.push([prefix, value]);
}
return output;
}
for (const [key, nestedValue] of Object.entries(value)) {
const nextKey = prefix ? `${prefix}.${key}` : key;
if (
nestedValue &&
typeof nestedValue === "object" &&
!Array.isArray(nestedValue)
) {
flattenDisplayRecord(nestedValue, nextKey, output);
} else {
output.push([nextKey, nestedValue]);
}
}
return output;
}
function formatDisplayValue(value) {
if (value === null || value === undefined || value === "") {
return "-";
}
if (Array.isArray(value) || typeof value === "object") {
return JSON.stringify(value);
}
return String(value);
}
function parseDatePreservingDateOnly(value) {
if (value instanceof Date) {
return value;
}
if (typeof value === "string") {
const dateOnlyMatch = value.trim().match(/^(\d{4})-(\d{2})-(\d{2})$/);
if (dateOnlyMatch) {
return new Date(
Number(dateOnlyMatch[1]),
Number(dateOnlyMatch[2]) - 1,
Number(dateOnlyMatch[3])
);
}
}
return new Date(value);
}
function getErrorMessage(error) {
if (error instanceof Error) {
return error.message;
}
return "Unexpected error while fetching live data.";
}
function escapeHtml(value) {
return String(value ?? "")
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#39;");
}
function lockScroll() {
document.documentElement.classList.add(CLASSES.modalOpen);
document.body.classList.add(CLASSES.modalOpen);
}
function unlockScroll() {
document.documentElement.classList.remove(CLASSES.modalOpen);
document.body.classList.remove(CLASSES.modalOpen);
}
function removeModal() {
const overlay = document.getElementById(IDS.overlay);
window.clearTimeout(closeTimer);
restoreFocusTarget = null;
if (overlay) {
overlay.remove();
}
}
function handleGlobalKeydown(event) {
if (event.key !== "Escape") {
return;
}
if (appState.actionFormModal) {
appState.actionFormModal = null;
renderModal();
return;
}
if (appState.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) {
if (event.source !== window) {
return;
}
const message = event.data;
if (!message || message.source !== BRIDGE.source) {
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 observePage() {
const observer = new MutationObserver(() => {
scheduleSync();
});
observer.observe(document.documentElement, {
childList: true,
subtree: true,
});
}
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);
}
window.addEventListener("message", handleBridgeMessage);
document.addEventListener("keydown", handleGlobalKeydown);
void ensurePageBridge();
observePage();
observeHistory();
scheduleSync();
})();