(() => { 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", }; 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(), 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") { shiftCalendarMonth(-1); return; } if (action === "calendar-next") { shiftCalendarMonth(1); return; } if (action === "calendar-today") { goToCurrentCalendarMonth(); 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}"]`; } 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 ` `; } function renderMainContent() { if (appState.status === "loading") { return `

Loading data

Building the workload dataset

${escapeHtml(appState.loadingMessage)}

Overall progress
${formatProgressPercent(appState.loadingProgress || 0)}
`; } if (appState.status === "error") { return `

Request failed

Unable to assemble the dashboard

${escapeHtml(appState.errorMessage)}

`; } if (!appState.dataset) { return `

Ready

Start the live data extraction

No cached snapshot is available yet. Use Refresh data to fetch the current user, customers, workloads, actions and service requests.

`; } const { user, summary } = appState.dataset; return `
${renderMetricCard("Total clients", formatWholeNumber(summary.totalCustomers), "Loaded from listSummary")} ${renderMetricCard("Total workloads", formatWholeNumber(summary.totalWorkloads), "Excluding Won workloads")} ${renderMetricCard("Total (ACR)", formatCurrency(summary.totalEligibleAcr), "Across eligible workloads")} ${renderMetricCard("Pending SRs", formatWholeNumber(summary.pendingServiceRequests.length), "SVC_PENDING_DELIVERY")}
${summary.stageIndicators.map((indicator) => renderStageIndicator(indicator)).join("")}
${renderBooleanIndicator( "Status de SRs", "Workloads com SR", summary.srStatus.trueCount, "Workloads sem SR", summary.srStatus.falseCount, "sr" )} ${renderBooleanIndicator( "Status de Consumption Plan/action", "Workloads com action", summary.actionStatus.trueCount, "Workloads sem action", summary.actionStatus.falseCount, "action" )}
${renderSrHoursAndCalendarSection(summary)}
`; } function getExtensionVersion() { try { return chrome.runtime.getManifest()?.version || "0.0.0"; } catch { return "0.0.0"; } } function renderMetricCard(label, value, detail) { return `

${escapeHtml(label)}

${escapeHtml(value)}

${escapeHtml(detail)}

`; } function renderStageIndicator(indicator) { return ` `; } function renderBooleanIndicator( title, trueLabel, trueCount, falseLabel, falseCount, scope ) { const total = trueCount + falseCount; const truePercent = total > 0 ? (trueCount / total) * 100 : 0; const falsePercent = total > 0 ? (falseCount / total) * 100 : 0; return `

${escapeHtml(title)}

${escapeHtml(trueLabel)}: ${formatPercent(truePercent)} ${escapeHtml(falseLabel)}: ${formatPercent(falsePercent)}
`; } function renderSrHoursAndCalendarSection(summary) { const calendar = buildWorkloadStartCalendar( summary.allWorkloads, appState.calendarYear, appState.calendarMonth ); return `
${renderSrHoursIndicator( "SRs 0-4h", "Horas apontadas entre 0 e 4", summary.srHours.betweenZeroAndFour.length, "0-4" )} ${renderSrHoursIndicator( "SRs >4h", "Horas apontadas acima de 4", summary.srHours.aboveFour.length, "above-4" )}
${renderWorkloadStartCalendar(calendar)}
`; } function renderSrHoursIndicator(label, detail, count, value) { return ` `; } function renderWorkloadStartCalendar(calendar) { return `

Workload start calendar

${escapeHtml(calendar.monthLabel)}

${formatWholeNumber(calendar.totalVisibleWorkloads)} starts
${["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"] .map((day) => `${day}`) .join("")}
${calendar.days.map((day) => renderCalendarDay(day)).join("")}
`; } function renderCalendarDay(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 ` `; } function renderWorkloadTable(workloads) { return `
${workloads.map((workload) => renderWorkloadTableRow(workload)).join("")}
Opportunity Customer Workload ACR Type Tem SR? Tem Action?
`; } function renderWorkloadTableRow( workload, includeConsumptionStartDate = false, includeRampMonths = false ) { const hasRampMismatch = includeRampMonths && hasRampForecastMismatch(workload); return ` ${renderOpportunityLink(workload)} ${escapeHtml(workload.customerName || "")} ${renderWorkloadLink(workload, hasRampMismatch)} ${ includeConsumptionStartDate ? `${escapeHtml(formatDate(workload.consumptionStartDate) || "-")}` : "" } ${ includeRampMonths ? `${renderRampMonthsButton(workload)}` : "" } ${formatCurrency(workload.adjustedACR)} ${escapeHtml(workload.opportunityForecastTypeGroup)} ${renderBooleanBadge(workload.hasSR)} ${renderHasActionCell(workload)} `; } function renderRampMonthsButton(workload) { return ` `; } function renderBooleanBadge(value) { const isTrue = Boolean(value); const variantClass = isTrue ? "is-success" : "is-danger"; const label = isTrue ? "Sim" : "Não"; return ` ${label} `; } function renderHasActionCell(workload) { if (workload.hasAction) { return renderBooleanBadge(true); } return `
${renderBooleanBadge(false)}
`; } function renderInsightRow(label, value, detail) { return `

${escapeHtml(label)}

${escapeHtml(detail)}

${escapeHtml(value)}

`; } function renderDetailModal() { if (!appState.detailModal) { return ""; } const isServiceRequestDetail = appState.detailModal.type === "serviceRequests"; const detailItems = isServiceRequestDetail ? appState.detailModal.items || [] : getSortedDetailItems(); return `

${escapeHtml(appState.detailModal.label)}

${escapeHtml(appState.detailModal.title)}

${ isServiceRequestDetail ? `${formatWholeNumber(detailItems.length)} SRs` : `${formatWholeNumber(detailItems.length)} workloads Total (ACR): ${formatCurrency(appState.detailModal.totalAcr)}` }
${ isServiceRequestDetail ? renderServiceRequestDetailTable(detailItems) : renderDetailTable( detailItems, appState.detailModal.scope === "calendar-day" ) }
`; } function renderActionFormModal() { if (!appState.actionFormModal || !appState.dataset) { return ""; } const workload = findWorkloadById(appState.actionFormModal.workloadId); const userEmail = appState.dataset.user?.userEmail || ""; if (!workload) { return ""; } const modalError = cleanString(appState.actionFormModal.errorMessage); const isSubmitting = Boolean(appState.actionFormModal.isSubmitting); return `

Create workload action

Criar action

${renderOpportunityLink(workload)} ${escapeHtml(workload.customerName || "")} ${escapeHtml(workload.name || "Unnamed workload")}
${modalError ? `

${escapeHtml(modalError)}

` : ""}
`; } function renderRampComparisonModal() { const modal = appState.rampComparisonModal; if (!modal) { return ""; } const rows = modal.rows || []; const title = `${modal.workload.customerName || "Unknown customer"} | ${ modal.workload.name || "Unnamed workload" }${ modal.workload.description ? ` | ${modal.workload.description}` : "" }`; return `

Ramp forecast comparison

${escapeHtml(title)}

Ramp months: ${formatWholeNumber(modal.workload.rampMonths || 0)} Adjusted ACR: ${formatCurrency(modal.workload.adjustedACR)} Consumption start: ${escapeHtml( formatDate(modal.workload.consumptionStartDate) || "-" )}
${renderRampComparisonPivotTable(rows)}
`; } function renderRampComparisonPivotTable(rows) { if (!rows.length) { return `
Nenhum forecast encontrado para comparar.
`; } return `
${rows.map((row) => ``).join("")} ${rows .map( (row) => `` ) .join("")} ${rows .map( (row) => `` ) .join("")} ${rows .map( (row) => `` ) .join("")}
Period${escapeHtml(row.periodLabel)}
Current adjusted${formatCurrency(row.currentValue)}
Calculated ramp${formatCurrency(row.calculatedValue)}
Delta${formatCurrency(row.delta)}
`; } function renderForecastUpdateConfirmModal() { const modal = appState.forecastUpdateConfirmModal; if (!modal) { return ""; } return `

Confirmar atualização

Atualizar forecast pelo Calculated ramp?

Esta ação enviará ${formatWholeNumber( modal.payload.length )} meses calculados para o workload atual. Os valores atuais do forecast serão substituídos pelos valores da linha Calculated ramp.

${ modal.errorMessage ? `

${escapeHtml( modal.errorMessage )}

` : "" }
`; } function renderTimeEntriesDrawer() { const drawer = appState.timeEntriesDrawer; if (!drawer) { return ""; } const items = Array.isArray(drawer.items) ? drawer.items : []; return ` `; } function renderTimeEntriesDrawerBody(drawer, items) { if (drawer.status === "loading") { return `

Carregando

Buscando dados

Consultando o resumo de time entries na origem COMCIP.

`; } if (drawer.status === "error") { return `

Erro na requisição

${escapeHtml(drawer.errorMessage || "Não foi possível carregar os apontamentos.")}

`; } const groupedItems = groupTimeEntriesByResourceName(items); return `
${formatWholeNumber(items.length)} registros ${formatWholeNumber(groupedItems.length)} resources
${ items.length ? groupedItems.map((group) => renderTimeEntryGroup(group)).join("") : `
Nenhum registro retornado para esta SR.
` } `; } function renderTimeEntryGroup(group) { const totalTimeSpent = group.items.reduce((sum, item) => { return sum + getTimeEntryTimeSpent(item); }, 0); return `

resourceName

${escapeHtml(group.resourceName)}

${formatHours(totalTimeSpent)} total
${group.items.map((item) => renderTimeEntryRow(item)).join("")}
Activity Task type Time spent
Total ${formatHours(totalTimeSpent)}
`; } function renderTimeEntryRow(item) { return ` ${escapeHtml(getTimeEntryActivity(item) || "-")} ${escapeHtml(getTimeEntryTaskType(item) || "-")} ${formatHours(getTimeEntryTimeSpent(item))} `; } function renderDetailTable(items, includeConsumptionStartDate = false) { return `
${includeConsumptionStartDate ? "" : ""} ${items .map((workload) => renderWorkloadTableRow(workload, includeConsumptionStartDate, true) ) .join("")}
${renderSortButton("Opportunity", DETAIL_SORT_KEYS.opportunityNumber)} ${renderSortButton("Customer", DETAIL_SORT_KEYS.customerName)} ${renderSortButton("Workload", DETAIL_SORT_KEYS.workload)}consumptionStartDate${renderSortButton("Ramp months", DETAIL_SORT_KEYS.rampMonths, true)} ${renderSortButton("ACR", DETAIL_SORT_KEYS.adjustedACR, true)} ${renderSortButton("Type", DETAIL_SORT_KEYS.opportunityForecastTypeGroup)} ${renderSortButton("Tem SR?", DETAIL_SORT_KEYS.hasSR)} ${renderSortButton("Tem Action?", DETAIL_SORT_KEYS.hasAction)}
`; } 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 `
${items .map( (serviceRequest) => ` ` ) .join("")}
Customer SR Number Queue Service Pillar Oppty value Horas apontadas Customer deadline Oppty Status
${escapeHtml(serviceRequest.customerName || "-")} ${renderServiceRequestLink(serviceRequest)} ${escapeHtml(serviceRequest.queueName || "-")} ${escapeHtml(serviceRequest.serviceName || "-")} ${escapeHtml(serviceRequest.pillarLabel || "-")} ${formatCurrency(serviceRequest.opportunityValue)} ${renderReportedHoursLink(serviceRequest)} ${escapeHtml(formatDate(serviceRequest.customerDeadline) || "-")} ${escapeHtml(serviceRequest.opportunityStatusLabel || "-")}
`; } function renderSortButton(label, sortKey, isNumeric = false) { const currentKey = appState.detailModal?.sortKey || ""; const currentDirection = appState.detailModal?.sortDirection || "asc"; const indicator = currentKey === sortKey ? (currentDirection === "asc" ? " ↑" : " ↓") : ""; return ` `; } function renderOpportunityLink(workload) { const label = workload.opportunityNumber || "Opportunity"; if (!workload.opportunityId || !workload.opportunityNumber) { return escapeHtml(label); } const href = `https://eeho.fa.us2.oraclecloud.com/fscmUI/redwood/cx-sales/application/container/opportunities/opportunities-detail?id=${encodeURIComponent( workload.opportunityId )}&puid=${encodeURIComponent(workload.opportunityNumber)}`; return `${escapeHtml(label)}`; } function renderServiceRequestLink(serviceRequest) { const srNumber = cleanString(serviceRequest?.srNumber); if (!srNumber) { return "-"; } const href = `https://eeho.fa.us2.oraclecloud.com/fscmUI/redwood/service/ec/container/sr/edit?srNumber=${encodeURIComponent( srNumber )}`; return `${escapeHtml(srNumber)}`; } function renderReportedHoursLink(serviceRequest) { const srNumber = cleanString(serviceRequest?.srNumber); const label = formatHours(serviceRequest?.totalHoursWorked); if (!srNumber) { return escapeHtml(label); } return ` `; } function renderWorkloadLink(workload, showRampAlert = false) { const description = workload.description ? ` | ${workload.description}` : ""; const label = `${workload.name || "Unnamed workload"}${description}`; if (!workload.workloadId) { return `${escapeHtml(label)}${renderRampAlertButton(workload, showRampAlert)}`; } const href = `https://spa.oracle.com/oalcrm/web/api/g2m-consumer-application/ui/index.html?ojr=workload_workbench/workload_details;workloadId=${encodeURIComponent( workload.workloadId )}`; return ` ${escapeHtml(label)} ${renderRampAlertButton(workload, showRampAlert)} `; } function renderRampAlertButton(workload, showRampAlert) { if (!showRampAlert || !workload.workloadId) { return ""; } return ` `; } function getHeaderNote() { if (appState.status === "loading") { return appState.loadingMessage || "Loading"; } if (appState.status === "error") { return "Extraction failed"; } if (appState.dataset) { return `${appState.dataset.user.userEmail} · ${formatWholeNumber( appState.dataset.summary.eligibleWorkloads.length )} eligible workloads`; } return "Ready"; } function getHeaderSummary() { if (appState.status === "loading") { return appState.loadingMessage || "Loading"; } if (appState.status === "error") { return "Extraction failed"; } if (appState.dataset) { return `${appState.dataset.user.userEmail} | ${formatWholeNumber( appState.dataset.summary.eligibleWorkloads.length )} eligible workloads`; } return "Ready"; } function openModal() { const overlay = ensureModal(); if (!overlay) { return; } window.clearTimeout(closeTimer); restoreFocusTarget = document.activeElement instanceof HTMLElement ? document.activeElement : null; overlay.hidden = false; overlay.setAttribute("aria-hidden", "false"); lockScroll(); window.requestAnimationFrame(() => { overlay.classList.add(CLASSES.overlayOpen); const primaryAction = overlay.querySelector("[data-action='close']"); primaryAction?.focus(); }); if (!appState.dataset && appState.status !== "loading") { void hydrateDatasetFromCache(); } } function closeModal() { const overlay = document.getElementById(IDS.overlay); if (!overlay || overlay.hidden) { return; } overlay.classList.remove(CLASSES.overlayOpen); overlay.setAttribute("aria-hidden", "true"); unlockScroll(); appState.detailModal = null; appState.actionFormModal = null; appState.rampComparisonModal = null; appState.forecastUpdateConfirmModal = null; appState.timeEntriesDrawer = null; 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) { const today = new Date(); const monthStart = new Date(year, month, 1); const calendarStart = new Date(monthStart); calendarStart.setDate(monthStart.getDate() - monthStart.getDay()); 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: 42 }, (_, 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: date.getMonth() === month, isToday: dateKey === getLocalDateKey(today), workloads: workloadsForDay, }; }); return { monthLabel: new Intl.DateTimeFormat("en-US", { month: "long", year: "numeric", }).format(monthStart), days, totalVisibleWorkloads: days.reduce( (sum, day) => sum + day.workloads.length, 0 ), }; } 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 shiftCalendarMonth(delta) { const nextDate = new Date(appState.calendarYear, appState.calendarMonth + delta, 1); appState.calendarYear = nextDate.getFullYear(); appState.calendarMonth = nextDate.getMonth(); renderModal(); } function goToCurrentCalendarMonth() { const today = new Date(); appState.calendarYear = today.getFullYear(); appState.calendarMonth = today.getMonth(); renderModal(); } async function submitActionForm(form) { if (!appState.dataset || !appState.actionFormModal) { return; } if (!form.reportValidity()) { return; } const formData = new FormData(form); const parentId = cleanString(formData.get("parentId")); const ownerEmail = cleanString(formData.get("ownerEmail")); const startDate = toIsoDateString(formData.get("startDate")); const endDate = toIsoDateString(formData.get("endDate")); const payload = { parentType: "WORKLOAD", parentId, action: { ownerEmail, role: "CLOUD_ARCHITECT", name: "SUCCESS_PLAN_CONSUM_RVW", team: cleanString(formData.get("team")), tags: "", startDate, endDate, status: "COMPLETE", complexity: cleanString(formData.get("complexity")), notes: [], objective: "", createdBy: cleanString(formData.get("createdBy")), updatedBy: cleanString(formData.get("updatedBy")), }, }; try { appState.actionFormModal = { ...appState.actionFormModal, isSubmitting: true, errorMessage: "", }; renderModal(); await fetchJson(API_ENDPOINTS.workloadActions, { method: "POST", headers: { "content-type": "application/json; charset=UTF-8", }, body: JSON.stringify(payload), }); await applyCreatedActionToDataset(payload); appState.actionFormModal = null; renderModal(); } catch (error) { appState.actionFormModal = { ...appState.actionFormModal, isSubmitting: false, errorMessage: getErrorMessage(error), }; renderModal(); } } async function applyCreatedActionToDataset(payload) { if (!appState.dataset) { return; } const updatedCustomers = (appState.dataset.customers || []).map((customer) => ({ ...customer, workloads: (customer.workloads || []).map((workload) => { if (workload.workloadId !== payload.parentId) { return workload; } return { ...workload, hasAction: true, actions: [ ...(workload.actions || []), { owner: payload.action.ownerEmail, role: payload.action.role, name: payload.action.name, team: payload.action.team, startDate: payload.action.startDate, endDate: payload.action.endDate, complexity: payload.action.complexity, }, ], }; }), })); const exportPayload = createExportPayload( appState.dataset.user, updatedCustomers, appState.dataset.exportPayload?.pendingServiceRequests || [], appState.dataset.exportPayload?.resourceCurrentUser || null ); const nextDataset = createDatasetSnapshot(exportPayload); const previousDetailModal = appState.detailModal ? { scope: appState.detailModal.scope, value: appState.detailModal.value, sortKey: appState.detailModal.sortKey, sortDirection: appState.detailModal.sortDirection, } : null; appState.dataset = nextDataset; await writeCachedDataset(exportPayload); if (previousDetailModal?.scope) { restoreDetailModal(previousDetailModal); } } function restoreDetailModal(detailState) { const eligibleWorkloads = appState.dataset?.summary?.eligibleWorkloads || []; let items = []; let label = ""; let title = ""; if (detailState.scope === "stage") { items = eligibleWorkloads.filter( (workload) => workload.opportunityForecastTypeGroup === detailState.value ); label = `Opportunity type: ${detailState.value}`; title = `${detailState.value} workloads`; } else if (detailState.scope === "sr") { const flag = detailState.value === "true"; items = eligibleWorkloads.filter((workload) => workload.hasSR === flag); label = "Status de SRs"; title = flag ? "Workloads com SR" : "Workloads sem SR"; } else if (detailState.scope === "action") { const flag = detailState.value === "true"; items = eligibleWorkloads.filter((workload) => workload.hasAction === flag); label = "Status de Consumption Plan/action"; title = flag ? "Workloads com action" : "Workloads sem action"; } else if (detailState.scope === "calendar-day") { const allWorkloads = appState.dataset?.summary?.allWorkloads || []; items = allWorkloads.filter((workload) => { return getLocalDateKey(workload.consumptionStartDate) === detailState.value; }); label = "Workload start calendar"; title = `Workloads starting ${formatDate(detailState.value) || detailState.value}`; } appState.detailModal = { label, title, items, totalAcr: items.reduce((sum, workload) => sum + workload.adjustedACR, 0), sortKey: detailState.sortKey || DETAIL_SORT_KEYS.adjustedACR, sortDirection: detailState.sortDirection || "desc", scope: detailState.scope, value: detailState.value, type: "workloads", }; } function toggleDetailSort(sortKey) { if (!appState.detailModal) { return; } const nextDirection = appState.detailModal.sortKey === sortKey && appState.detailModal.sortDirection === "asc" ? "desc" : "asc"; appState.detailModal = { ...appState.detailModal, sortKey, sortDirection: nextDirection, }; renderModal(); } function getSortedDetailItems() { if (!appState.detailModal) { return []; } return sortWorkloads( appState.detailModal.items, appState.detailModal.sortKey, appState.detailModal.sortDirection ); } function sortWorkloads(items, sortKey, sortDirection = "asc") { const factor = sortDirection === "desc" ? -1 : 1; return [...items].sort((left, right) => { const comparison = compareWorkloadValues( getSortableWorkloadValue(left, sortKey), getSortableWorkloadValue(right, sortKey) ); if (comparison !== 0) { return comparison * factor; } return compareWorkloadValues( getSortableWorkloadValue(left, DETAIL_SORT_KEYS.workload), getSortableWorkloadValue(right, DETAIL_SORT_KEYS.workload) ); }); } function getSortableWorkloadValue(workload, sortKey) { switch (sortKey) { case DETAIL_SORT_KEYS.opportunityNumber: return cleanString(workload.opportunityNumber); case DETAIL_SORT_KEYS.customerName: return cleanString(workload.customerName); case DETAIL_SORT_KEYS.workload: return `${cleanString(workload.name)}|${cleanString(workload.description)}`; case DETAIL_SORT_KEYS.rampMonths: return toNumber(workload.rampMonths); case DETAIL_SORT_KEYS.adjustedACR: return toNumber(workload.adjustedACR); case DETAIL_SORT_KEYS.opportunityForecastTypeGroup: return cleanString(workload.opportunityForecastTypeGroup); case DETAIL_SORT_KEYS.hasSR: return Boolean(workload.hasSR); case DETAIL_SORT_KEYS.hasAction: return Boolean(workload.hasAction); default: return cleanString(workload.name); } } function compareWorkloadValues(left, right) { if (typeof left === "number" || typeof right === "number") { return toNumber(left) - toNumber(right); } if (typeof left === "boolean" || typeof right === "boolean") { return Number(Boolean(left)) - Number(Boolean(right)); } return String(left || "").localeCompare(String(right || ""), undefined, { numeric: true, sensitivity: "base", }); } function downloadFile(filename, content, contentType) { const blob = new Blob([content], { type: contentType }); const url = URL.createObjectURL(blob); const anchor = document.createElement("a"); anchor.href = url; anchor.download = filename; anchor.click(); window.setTimeout(() => { URL.revokeObjectURL(url); }, 1000); } function createExportHtml(dataset) { const json = JSON.stringify(dataset.exportPayload); return ` Arch Panel Dashboard
`; } function toggleTheme() { appState.theme = appState.theme === "dark" ? "light" : "dark"; setStoredTheme(appState.theme); renderModal(); } function toggleSidebar() { appState.sidebarCollapsed = !appState.sidebarCollapsed; setStoredSidebarCollapsed(appState.sidebarCollapsed); renderModal(); } function applyDialogTheme(overlay) { const dialog = overlay?.querySelector(`#${IDS.dialog}`); if (!dialog) { return; } dialog.setAttribute("data-theme", appState.theme); } function ensureStyles() { if (document.getElementById(IDS.style)) { return; } const style = document.createElement("style"); style.id = IDS.style; style.textContent = ` html.${CLASSES.modalOpen}, body.${CLASSES.modalOpen} { overflow: hidden !important; } .arch-panel-extension-entry { --arch-panel-bg: rgba(255, 255, 255, 0.12); --arch-panel-bg-hover: rgba(255, 255, 255, 0.2); --arch-panel-border: rgba(255, 255, 255, 0.26); --arch-panel-fg: #ffffff; --arch-panel-focus: rgba(255, 255, 255, 0.72); --arch-panel-font: inherit; --arch-workbench-header: #4b335d; --arch-workbench-accent: #6c4d80; display: inline-flex; align-items: center; margin-inline-end: 12px; flex: 0 0 auto; } .arch-panel-extension-button { appearance: none; border: 1px solid var(--arch-panel-border); background: var(--arch-panel-bg); color: var(--arch-panel-fg); border-radius: 999px; min-height: 36px; padding: 0 14px; display: inline-flex; align-items: center; justify-content: center; font-family: var(--arch-panel-font); font-size: 13px; font-weight: 600; line-height: 1; letter-spacing: 0.01em; cursor: pointer; transition: background-color 160ms ease, border-color 160ms ease, transform 160ms ease; } .arch-panel-extension-button:hover { background: var(--arch-panel-bg-hover); } .arch-panel-extension-button:active { transform: translateY(1px); } .arch-panel-extension-button:focus-visible, .arch-panel-extension-close:focus-visible, .arch-panel-extension-theme-toggle:focus-visible, .arch-panel-extension-sidebar-button:focus-visible, .arch-panel-extension-utility-button:focus-visible, .arch-panel-extension-summary-card-button:focus-visible, .arch-panel-extension-sort-button:focus-visible, .arch-panel-extension-indicator-card:focus-visible, .arch-panel-extension-row-link:focus-visible { outline: 2px solid var(--arch-panel-focus); outline-offset: 2px; } .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-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-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: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 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, "&") .replace(//g, ">") .replace(/"/g, """) .replace(/'/g, "'"); } function lockScroll() { document.documentElement.classList.add(CLASSES.modalOpen); document.body.classList.add(CLASSES.modalOpen); } function unlockScroll() { document.documentElement.classList.remove(CLASSES.modalOpen); document.body.classList.remove(CLASSES.modalOpen); } function removeModal() { const overlay = document.getElementById(IDS.overlay); window.clearTimeout(closeTimer); restoreFocusTarget = null; if (overlay) { overlay.remove(); } } function handleGlobalKeydown(event) { if (event.key !== "Escape") { return; } if (appState.actionFormModal) { appState.actionFormModal = null; renderModal(); return; } if (appState.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(); })();