(function () { "use strict"; const TILE_ID = "c_5c719a88b5624b1eaf81defe2c2ex4x5"; const TILE_LABEL_ID = `${TILE_ID}_0`; const SALES_GROUP_SELECTOR = "#yourapps_groupNode_sales"; const ADD_ICON_SELECTOR = ".flat-grid-cell.flat-grid-cell-addicon"; const MODAL_ID = "opportunities-extension-modal"; const STYLE_ID = "opportunities-extension-redwood-styles"; const ESCAPE_LISTENER_KEY = "opportunitiesExtensionEscapeListener"; const DEBUG_PANEL_ID = "opportunities-extension-debug-panel"; const OPPORTUNITIES_TABLE_ID = "opportunities-extension-table"; const STAGE_FILTERS_ID = "opportunities-extension-stage-filters"; const STATUS_FILTERS_ID = "opportunities-extension-status-filters"; const CUSTOMER_FILTER_BUTTON_ID = "opportunities-extension-customer-filter-button"; const CUSTOMER_FILTER_PANEL_ID = "opportunities-extension-customer-filter-panel"; const CUSTOMER_FILTER_SEARCH_ID = "opportunities-extension-customer-filter-search"; const CUSTOMER_FILTER_SELECT_ALL_ID = "opportunities-extension-customer-filter-select-all"; const CUSTOMER_FILTER_LIST_ID = "opportunities-extension-customer-filter-list"; const OWNER_FILTER_BUTTON_ID = "opportunities-extension-owner-filter-button"; const OWNER_FILTER_PANEL_ID = "opportunities-extension-owner-filter-panel"; const OWNER_FILTER_SEARCH_ID = "opportunities-extension-owner-filter-search"; const OWNER_FILTER_SELECT_ALL_ID = "opportunities-extension-owner-filter-select-all"; const OWNER_FILTER_LIST_ID = "opportunities-extension-owner-filter-list"; const TABLE_SEARCH_ID = "opportunities-extension-table-search"; const STAGE_DASHBOARD_ID = "opportunities-extension-stage-dashboard"; const PRODUCTS_PROGRESS_ID = "opportunities-extension-products-progress"; const PRODUCTS_CACHE_TIMESTAMP_ID = "opportunities-extension-products-cache-timestamp"; const FILTER_PREFERENCES_STORAGE_KEY = "opportunities-extension-filter-preferences"; const THEME_STORAGE_KEY = "opportunities-extension-theme"; const OPPORTUNITY_TYPE_PREFERENCES_STORAGE_KEY = "opportunities-extension-opportunity-type-preferences"; const PREFERENCES_MODAL_ID = "opportunities-extension-preferences-modal"; const ACCOUNT_PLANS_MODAL_ID = "opportunities-extension-account-plans-modal"; const PRODUCTS_MESSAGE_LISTENER_KEY = "opportunitiesExtensionProductsMessageListener"; const OPPORTUNITY_DETAIL_URL = "https://eeho.fa.us2.oraclecloud.com/fscmUI/redwood/cx-sales/application/container/opportunities/opportunities-detail?puid="; const ACCOUNT_DETAIL_URL = "https://eeho.fa.us2.oraclecloud.com/fscmUI/redwood/cx-sales/application/container/accounts/accounts-detail?id="; const DEBUG_BODY_LIMIT = 12000; const OPPORTUNITIES_PAGE_LIMIT = 15; const DEFAULT_OPPTY_PRODUCTS_CACHE_HOURS = 24; const TOKEN_RELAY_URL = "https://eeho.fa.us2.oraclecloud.com/fscmRestApi/tokenrelay"; const OPPORTUNITIES_QUERY_URL = "https://eeho.fa.us2.oraclecloud.com/crmRestApi/searchResources/11.13.18.05/custom-actions/queries"; const AUTH_STATUS = { idle: "idle", pending: "pending", authenticated: "authenticated", unauthenticated: "unauthenticated" }; const STAGE_OPTIONS = ["SQL", "PIPELINE", "UPSIDE", "FORECAST", "WON"]; const STATUS_OPTIONS = ["OPEN", "CLOSED", "LOST", "WON"]; const OPPORTUNITY_TYPE_OPTIONS = [ { label: "All records I can see", value: "ORA_ALLOPTIES" }, { label: "I am credit receiver", value: "ORA_CREDITRECEIVER_ISME" }, { label: "I am on the team", value: "ORA_MYSALESTEAMOPTIES" }, { label: "I or my subordinate is a credit receiver", value: "ORA_CREDITRECEIVER_MYORG" }, { label: "I own", value: "ORA_MYOPTIES" }, { label: "My subordinates are on the team", value: "ORA_MYSUBORDSSALESTEAMOPTIES" }, { label: "My subordinates own", value: "ORA_MYSUBORDINATESOPTIES" }, { label: "My territory", value: "ORA_MYASSGTERROPTIES" }, { label: "My territory hierarchy", value: "ORA_MYASSGDESCTERROPTIES" }, { label: "My territory membership", value: "ORA_MYASSGMEMBTERROPTIES" }, { label: "My territory membership hierarchy", value: "ORA_MYASSGMEMBDESCTERROPTIES" } ]; const DEFAULT_OPPORTUNITY_TYPE_VALUES = [ "ORA_MYSALESTEAMOPTIES", "ORA_MYASSGTERROPTIES", "ORA_CREDITRECEIVER_ISME" ]; const OPPTY_PRODUCTS_CACHE_OPTIONS = [ { label: "1 hour", value: 1 }, { label: "6 hours", value: 6 }, { label: "12 hours", value: 12 }, { label: "24 hours (default)", value: 24 }, { label: "48 hours", value: 48 }, { label: "7 days", value: 168 } ]; let tokenRelayRequest = null; let consumerTokenServiceRequest = null; let accessToken = ""; let periodRequestVersion = 0; let authStatus = AUTH_STATUS.idle; let requestLog = []; let debugSecretsVisible = false; let selectedStages = new Set(STAGE_OPTIONS); let selectedStatuses = new Set(STATUS_OPTIONS); let selectedCustomerKeys = new Set(); let customerSearch = ""; let selectedOwnerKeys = new Set(); let ownerSearch = ""; let tableSearch = ""; let selectedOpportunityTypeValues = new Set(DEFAULT_OPPORTUNITY_TYPE_VALUES); let opptyProductsCacheHours = DEFAULT_OPPTY_PRODUCTS_CACHE_HOURS; let currentProductsRequestId = ""; let productsRenderQueued = false; let opptyProductsByNumber = new Map(); let expandedProductOptyNumbers = new Set(); let productsProgress = { state: "idle", completed: 0, total: 0, failed: 0, cacheHits: 0 }; let productsLastCachedAt = 0; let accountPlansRequestState = new Map(); let hasSavedCustomerFilter = false; let hasSavedOwnerFilter = false; let customerFilterInitialized = false; let ownerFilterInitialized = false; let stageDashboardAmounts = new Map([["TOTAL", 0], ...STAGE_OPTIONS.map((stage) => [stage, 0])]); let opportunitiesTableState = { items: [], status: "idle", message: "", sortKey: "", sortDirection: "ascending" }; const PERIOD_OPTIONS = [ "Current Quarter", "Next Quarter", "Previous Quarter", "Current Fiscal Year", "4 Rolling Quarters (CQ + 3)", "RENEWALS (Current + Past Due)" ]; const OPPORTUNITIES_COLUMNS = [ { key: "name", label: "Name", value: (item) => item.Name, display: (item) => item.Name }, { key: "optyNumber", label: "Opty Number", value: (item) => item.OptyNumber, display: (item) => item.OptyNumber }, { key: "customer", label: "Customer", value: (item) => getNestedValue(item, ["CustomerAccount", "PartyUniqueName"]), display: (item) => getNestedValue(item, ["CustomerAccount", "PartyUniqueName"]) }, { key: "owner", label: "Owner", value: getOwnerName, display: getOwnerName }, { key: "revenue", label: "Amount", value: (item) => getNestedValue(item, ["PrimaryRevenue", "RevnAmount"]), display: formatRevenue }, { key: "closeDate", label: "Close Date", value: (item) => item.EffectiveDate, display: (item) => formatOracleResponseDate(item.EffectiveDate) }, { key: "status", label: "Status", value: (item) => item.StatusCode, display: (item) => item.StatusCode }, { key: "winProbability", label: "Win Probability", value: (item) => getNestedValue(item, ["PrimaryRevenue", "WinProb"]), display: formatWinProbability }, { key: "stage", label: "Stage", value: (item) => item.ForecastGroup_c, display: (item) => item.ForecastGroup_c }, { key: "lastUpdateDate", label: "Last Update Date", value: (item) => item.LastUpdateDate, display: (item) => formatOracleResponseDate(item.LastUpdateDate, true) }, { key: "actions", label: "Actions", value: () => "", display: () => "", sortable: false } ]; const ALLOWED_PATHS = [ "/hcmUI/faces/FuseWelcome", "/fscmUI/faces/FuseWelcome" ]; if (!isAllowedPage()) { return; } ensureExtensionStyles(); ensureProductsBackgroundMessageHandler(); function createOpportunitiesTile() { const wrapper = document.createElement("div"); wrapper.className = "flat-grid-cell"; const item = document.createElement("div"); item.id = TILE_ID; item.className = "app-nav-item opportunities-extension-tile"; item.setAttribute("filmstrip", "Opportunities Extension"); item.setAttribute("page", "undefined"); item.setAttribute("index", "0"); item.setAttribute("type", "subcluster"); item.setAttribute("title", "Opportunities Extension"); item.setAttribute("group", "groupNode_tools"); item.setAttribute("destinationurl", "https://gxpap.oracle.com/ords/pgxpap/f?p=138"); item.setAttribute("targetframe", "_blank"); item.setAttribute("isdesturlexist", "true"); item.setAttribute("role", "presentation"); const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg"); svg.setAttribute("viewBox", "0 0 48 48"); svg.setAttribute("style", "fill:currentColor"); svg.setAttribute("class", "svg-nav suiicon svg-bkgd09"); svg.setAttribute("data-icon", "navi_reportsearch"); svg.setAttribute("role", "presentation"); svg.setAttribute("focusable", "false"); appendPath(svg, "svg-shortcut", "M28 42.5l-3 2.7v-1.7c-.4 0-1.4 0-2.5.6-1.3 1-1.5 1.6-1.5 1.6s-.4-1.2.8-2.7c1.2-1.6 2.6-1.7 3.2-1.6v-1.6l3 2.7z"); appendPath(svg, "svg-cluster", "M28.5 41.3c.6 0 1.2.5 1.2 1.2s-.6 1.2-1.2 1.2-1.2-.5-1.2-1.2.5-1.2 1.2-1.2zm-4 0c.6 0 1.2.5 1.2 1.2s-.6 1.2-1.2 1.2c-.7 0-1.2-.5-1.2-1.2s.5-1.2 1.2-1.2zm-4 0c.7 0 1.2.5 1.2 1.2s-.5 1.2-1.2 1.2-1.2-.5-1.2-1.2.5-1.2 1.2-1.2z"); appendPath(svg, "svg-icon15", "M16 31l-1.6-1-3.4 6.5s0 1 .5 1.4c.5.2 1.4-.4 1.4-.4l3-6.7z"); appendPath(svg, "svg-icon03", "M36 10H12c-.8 0-2 1.2-2 2v20c0 .4.2.8.5 1l2-3.6c-1-1.4-1.6-3-1.6-5 0-4.2 3.3-7.6 7.4-7.6H20V16h2v1.7c.7.4 1.3 1 1.8 1.5.6.5 1 1 1.3 1.8h4v7h-4c-.3.7-.8 1.4-1.4 2H35v2H20.3l-2 .2H18L17 34h19c.8 0 2-1.2 2-2V12c0-.8-1.2-2-2-2zm-23 4v-2h22v2H13zm22 14h-5V17h5v11z"); appendPath(svg, "svg-icon12", "M18.5 19c-3 0-5.5 2.5-5.5 5.5s2.5 5.5 5.5 5.5 5.5-2.5 5.5-5.5-2.5-5.5-5.5-5.5zm0 9c-2 0-3.5-1.6-3.5-3.5 0-2 1.6-3.5 3.5-3.5s3.5 1.6 3.5 3.5c0 2-1.6 3.5-3.5 3.5z"); appendPath(svg, "svg-outline", "M35 34.56H13a2.7 2.7 0 0 1-3-3V14a2.76 2.76 0 0 1 3-3h22a2.74 2.74 0 0 1 3 3v17.56a2.68 2.68 0 0 1-3 3zM16.98 22.32a4.72 4.72 0 1 0 4.73 4.73 4.72 4.72 0 0 0-4.73-4.73zM24 25h3.47v4.72H24V25zm5.78-4.66h4.69v9.38h-4.69v-9.38zM13.5 14.5h20.9v2.4H13.5v-2.4zm6.9 17.59l-.01-1.94zm-2.04-12.75l-5.35-.02zm2.13 12.67H35.7zm-.09-8.05L20.39 19z"); appendPath(svg, "svg-outline", "M16.98 31.72a4.68 4.68 0 1 0-4.75-4.67 4.74 4.74 0 0 0 4.75 4.67zm-1.44-.5l-3.6 6.83zM8 40"); const link = document.createElement("a"); link.id = TILE_LABEL_ID; link.className = "app-nav-label flat-grid-nav-label"; link.href = "#"; link.textContent = "Opportunities Extension"; item.append(svg, link); wrapper.append(item); wrapper.addEventListener("click", handleTileClick); return wrapper; } function appendPath(svg, className, d) { const path = document.createElementNS("http://www.w3.org/2000/svg", "path"); path.setAttribute("class", className); path.setAttribute("d", d); svg.append(path); } function handleTileClick(event) { event.preventDefault(); event.stopPropagation(); openOpportunitiesModal(); } function openOpportunitiesModal() { ensureExtensionStyles(); debugSecretsVisible = false; const existingModal = document.getElementById(MODAL_ID); if (existingModal) { existingModal.hidden = false; existingModal.querySelector("select").focus(); document.documentElement.classList.add("opportunities-extension-scroll-lock"); ensureEscapeKeyHandler(); requestTokenRelayOnce().catch(() => {}); return; } const overlay = document.createElement("div"); requestLog = []; tokenRelayRequest = null; consumerTokenServiceRequest = null; accessToken = ""; periodRequestVersion = 0; authStatus = AUTH_STATUS.idle; resetOpportunitiesTable(); selectedOpportunityTypeValues = new Set(getSavedOpportunityTypeValues()); opptyProductsCacheHours = getSavedOpptyProductsCacheHours(); overlay.id = MODAL_ID; overlay.className = "opportunities-extension-modal"; overlay.setAttribute("data-theme", getSavedTheme()); overlay.setAttribute("role", "dialog"); overlay.setAttribute("aria-modal", "true"); overlay.setAttribute("aria-labelledby", "opportunities-extension-title"); overlay.setAttribute("aria-describedby", "opportunities-extension-subtitle"); const shell = document.createElement("section"); shell.className = "opportunities-extension-shell"; const header = document.createElement("header"); header.className = "opportunities-extension-header"; const title = document.createElement("h1"); title.id = "opportunities-extension-title"; title.textContent = "Opportunities Extension"; const subtitle = document.createElement("p"); subtitle.id = "opportunities-extension-subtitle"; subtitle.textContent = "Lista de oportunidades do HCM Opportunities List"; const authBadge = document.createElement("span"); authBadge.className = "opportunities-extension-auth-badge opportunities-extension-auth-badge-pending"; authBadge.setAttribute("data-auth-status", AUTH_STATUS.pending); authBadge.textContent = "Authenticating"; const subtitleRow = document.createElement("div"); subtitleRow.className = "opportunities-extension-subtitle-row"; const productsProgressBadge = document.createElement("span"); productsProgressBadge.id = PRODUCTS_PROGRESS_ID; productsProgressBadge.className = "opportunities-extension-products-progress"; productsProgressBadge.setAttribute("role", "status"); productsProgressBadge.hidden = true; const productsCacheTimestamp = document.createElement("span"); productsCacheTimestamp.id = PRODUCTS_CACHE_TIMESTAMP_ID; productsCacheTimestamp.className = "opportunities-extension-products-cache-timestamp"; productsCacheTimestamp.setAttribute("role", "status"); productsCacheTimestamp.hidden = true; subtitleRow.append(subtitle, authBadge, productsProgressBadge, productsCacheTimestamp); const titleBlock = document.createElement("div"); titleBlock.append(title, subtitleRow); const closeButton = document.createElement("button"); closeButton.type = "button"; closeButton.className = "opportunities-extension-icon-button"; closeButton.setAttribute("aria-label", "Fechar"); closeButton.textContent = "X"; closeButton.addEventListener("click", closeOpportunitiesModal); const themeButton = document.createElement("button"); themeButton.type = "button"; themeButton.className = "opportunities-extension-icon-button opportunities-extension-theme-toggle"; themeButton.addEventListener("click", () => { const nextTheme = overlay.getAttribute("data-theme") === "dark" ? "light" : "dark"; applyTheme(overlay, themeButton, nextTheme); }); applyTheme(overlay, themeButton, overlay.getAttribute("data-theme")); const preferencesButton = document.createElement("button"); preferencesButton.type = "button"; preferencesButton.className = "opportunities-extension-icon-button"; preferencesButton.setAttribute("aria-label", "Abrir preferências"); preferencesButton.title = "Preferences"; preferencesButton.append(createSettingsIcon()); preferencesButton.addEventListener("click", openPreferencesModal); const headerActions = document.createElement("div"); headerActions.className = "opportunities-extension-header-actions"; headerActions.append(themeButton, preferencesButton, closeButton); header.append(titleBlock, headerActions); const body = document.createElement("main"); body.className = "opportunities-extension-body"; const form = document.createElement("form"); form.className = "opportunities-extension-form"; const field = document.createElement("label"); field.className = "opportunities-extension-field"; const labelText = document.createElement("span"); labelText.textContent = "Reference"; const selectWrap = document.createElement("span"); selectWrap.className = "opportunities-extension-select-wrap"; const select = document.createElement("select"); select.name = "opportunityPeriod"; PERIOD_OPTIONS.forEach((optionLabel) => { const option = document.createElement("option"); option.value = optionLabel; option.textContent = optionLabel; select.append(option); }); select.addEventListener("change", () => { resetCustomerAndOwnerFiltersForPeriod(); saveFilterPreferences(select.value); requestOpportunitiesForPeriod(select.value); }); selectWrap.append(select); field.append(labelText, selectWrap); restoreFilterPreferences(select); const stageFilter = document.createElement("section"); stageFilter.className = "opportunities-extension-stage-filter"; const stageFilterLabel = document.createElement("span"); stageFilterLabel.className = "opportunities-extension-stage-filter-label"; stageFilterLabel.textContent = "Stage"; const stageFilters = document.createElement("div"); stageFilters.id = STAGE_FILTERS_ID; stageFilters.className = "opportunities-extension-stage-filter-controls"; stageFilters.setAttribute("role", "group"); stageFilters.setAttribute("aria-label", "Filter by stage"); stageFilter.append(stageFilterLabel, stageFilters); const customerFilter = document.createElement("section"); customerFilter.className = "opportunities-extension-customer-filter"; const customerFilterLabel = document.createElement("span"); customerFilterLabel.className = "opportunities-extension-customer-filter-label"; customerFilterLabel.textContent = "Customer"; const customerFilterButton = document.createElement("button"); customerFilterButton.id = CUSTOMER_FILTER_BUTTON_ID; customerFilterButton.type = "button"; customerFilterButton.className = "opportunities-extension-customer-filter-trigger"; customerFilterButton.setAttribute("aria-expanded", "false"); customerFilterButton.setAttribute("aria-controls", CUSTOMER_FILTER_PANEL_ID); customerFilterButton.addEventListener("click", toggleCustomerFilterPanel); const customerFilterPanel = document.createElement("section"); customerFilterPanel.id = CUSTOMER_FILTER_PANEL_ID; customerFilterPanel.className = "opportunities-extension-customer-filter-panel"; customerFilterPanel.hidden = true; const customerSearchInput = document.createElement("input"); customerSearchInput.id = CUSTOMER_FILTER_SEARCH_ID; customerSearchInput.className = "opportunities-extension-customer-filter-search"; customerSearchInput.type = "search"; customerSearchInput.placeholder = "Search customers"; customerSearchInput.setAttribute("aria-label", "Search customers"); customerSearchInput.addEventListener("input", () => { customerSearch = customerSearchInput.value; saveFilterPreferences(); renderCustomerFilterList(); }); const selectAllLabel = document.createElement("label"); selectAllLabel.className = "opportunities-extension-customer-filter-select-all"; const selectAllCheckbox = document.createElement("input"); selectAllCheckbox.id = CUSTOMER_FILTER_SELECT_ALL_ID; selectAllCheckbox.type = "checkbox"; selectAllCheckbox.addEventListener("change", () => { const customerOptions = getCustomerOptions(); selectedCustomerKeys = selectAllCheckbox.checked ? new Set(customerOptions.map((customer) => customer.key)) : new Set(); hasSavedCustomerFilter = true; saveFilterPreferences(); renderCustomerFilter(); renderOpportunitiesTable(); }); const selectAllText = document.createElement("span"); selectAllText.textContent = "Select all"; selectAllLabel.append(selectAllCheckbox, selectAllText); const customerFilterList = document.createElement("div"); customerFilterList.id = CUSTOMER_FILTER_LIST_ID; customerFilterList.className = "opportunities-extension-customer-filter-list"; customerFilterPanel.append(customerSearchInput, selectAllLabel, customerFilterList); customerFilter.append(customerFilterLabel, customerFilterButton, customerFilterPanel); const ownerFilter = document.createElement("section"); ownerFilter.className = "opportunities-extension-customer-filter"; const ownerFilterLabel = document.createElement("span"); ownerFilterLabel.className = "opportunities-extension-customer-filter-label"; ownerFilterLabel.textContent = "Owner"; const ownerFilterButton = document.createElement("button"); ownerFilterButton.id = OWNER_FILTER_BUTTON_ID; ownerFilterButton.type = "button"; ownerFilterButton.className = "opportunities-extension-customer-filter-trigger"; ownerFilterButton.setAttribute("aria-expanded", "false"); ownerFilterButton.setAttribute("aria-controls", OWNER_FILTER_PANEL_ID); ownerFilterButton.addEventListener("click", toggleOwnerFilterPanel); const ownerFilterPanel = document.createElement("section"); ownerFilterPanel.id = OWNER_FILTER_PANEL_ID; ownerFilterPanel.className = "opportunities-extension-customer-filter-panel"; ownerFilterPanel.hidden = true; const ownerSearchInput = document.createElement("input"); ownerSearchInput.id = OWNER_FILTER_SEARCH_ID; ownerSearchInput.className = "opportunities-extension-customer-filter-search"; ownerSearchInput.type = "search"; ownerSearchInput.placeholder = "Search owners"; ownerSearchInput.setAttribute("aria-label", "Search owners"); ownerSearchInput.addEventListener("input", () => { ownerSearch = ownerSearchInput.value; saveFilterPreferences(); renderOwnerFilterList(); }); const ownerSelectAllLabel = document.createElement("label"); ownerSelectAllLabel.className = "opportunities-extension-customer-filter-select-all"; const ownerSelectAllCheckbox = document.createElement("input"); ownerSelectAllCheckbox.id = OWNER_FILTER_SELECT_ALL_ID; ownerSelectAllCheckbox.type = "checkbox"; ownerSelectAllCheckbox.addEventListener("change", () => { const ownerOptions = getOwnerOptions(); selectedOwnerKeys = ownerSelectAllCheckbox.checked ? new Set(ownerOptions.map((owner) => owner.key)) : new Set(); hasSavedOwnerFilter = true; saveFilterPreferences(); renderOwnerFilter(); renderOpportunitiesTable(); }); const ownerSelectAllText = document.createElement("span"); ownerSelectAllText.textContent = "Select all"; ownerSelectAllLabel.append(ownerSelectAllCheckbox, ownerSelectAllText); const ownerFilterList = document.createElement("div"); ownerFilterList.id = OWNER_FILTER_LIST_ID; ownerFilterList.className = "opportunities-extension-customer-filter-list"; ownerFilterPanel.append(ownerSearchInput, ownerSelectAllLabel, ownerFilterList); ownerFilter.append(ownerFilterLabel, ownerFilterButton, ownerFilterPanel); const tableSearchField = document.createElement("label"); tableSearchField.className = "opportunities-extension-table-search-field"; const tableSearchLabel = document.createElement("span"); tableSearchLabel.textContent = "Search"; const tableSearchInput = document.createElement("input"); tableSearchInput.id = TABLE_SEARCH_ID; tableSearchInput.type = "search"; tableSearchInput.placeholder = "Search opportunities"; tableSearchInput.setAttribute("aria-label", "Search opportunities"); tableSearchInput.value = tableSearch; tableSearchInput.addEventListener("input", () => { tableSearch = tableSearchInput.value; saveFilterPreferences(); renderOpportunitiesTable(); }); tableSearchField.append(tableSearchLabel, tableSearchInput); const statusFilter = document.createElement("section"); statusFilter.className = "opportunities-extension-status-filter"; const statusFilterLabel = document.createElement("span"); statusFilterLabel.className = "opportunities-extension-status-filter-label"; statusFilterLabel.textContent = "Status"; const statusFilters = document.createElement("div"); statusFilters.id = STATUS_FILTERS_ID; statusFilters.className = "opportunities-extension-status-filter-controls"; statusFilters.setAttribute("role", "group"); statusFilters.setAttribute("aria-label", "Filter by status"); statusFilter.append(statusFilterLabel, statusFilters); form.addEventListener("submit", (event) => event.preventDefault()); form.append(field, customerFilter, ownerFilter, tableSearchField, statusFilter, stageFilter); body.append(form); const stageDashboard = document.createElement("section"); stageDashboard.id = STAGE_DASHBOARD_ID; stageDashboard.className = "opportunities-extension-stage-dashboard"; stageDashboard.setAttribute("aria-label", "Opportunity amount by stage"); body.append(stageDashboard); const tableSurface = document.createElement("section"); tableSurface.className = "opportunities-extension-table-surface"; const opportunitiesTable = document.createElement("table"); opportunitiesTable.id = OPPORTUNITIES_TABLE_ID; opportunitiesTable.className = "opportunities-extension-table"; opportunitiesTable.setAttribute("aria-label", "Opportunities"); tableSurface.append(opportunitiesTable); body.append(tableSurface); const debugPanel = document.createElement("aside"); debugPanel.id = DEBUG_PANEL_ID; debugPanel.className = "opportunities-extension-debug-panel"; debugPanel.hidden = true; body.append(debugPanel); shell.append(header, body); overlay.append(shell); document.body.append(overlay); document.documentElement.classList.add("opportunities-extension-scroll-lock"); ensureEscapeKeyHandler(); renderStageFilterButtons(); renderStatusFilterButtons(); renderCustomerFilter(); renderOwnerFilter(); renderOpportunitiesTable(); requestOpportunitiesForPeriod(select.value); select.focus(); } function requestTokenRelayOnce() { if (!tokenRelayRequest) { authStatus = AUTH_STATUS.pending; updateAuthBadge(authStatus); tokenRelayRequest = requestTokenRelay() .then((token) => { accessToken = token; authStatus = AUTH_STATUS.authenticated; requestConsumerTokenServiceOnce().catch(() => {}); return token; }) .catch((error) => { accessToken = ""; authStatus = AUTH_STATUS.unauthenticated; throw error; }) .finally(() => { updateAuthBadge(authStatus); }); } else { updateAuthBadge(authStatus); } return tokenRelayRequest; } function requestConsumerTokenServiceOnce() { if (!consumerTokenServiceRequest) { consumerTokenServiceRequest = sendRuntimeMessage({ type: "opportunitiesExtension.requestConsumerTokenService" }).then((response) => { appendBackgroundRequestLog(response && response.debugRequests); if (!response || !response.ok) { throw new Error(response && response.error ? response.error : "Unable to load consumer token service."); } return response.authorizations; }); } return consumerTokenServiceRequest; } async function requestTokenRelay() { let refreshResult = null; let refreshAttempted = false; while (true) { const xsrfTokenSource = await getXsrfToken(); if (!xsrfTokenSource.token) { if (refreshAttempted) { const requestEntry = createTokenRelayRequestEntry(xsrfTokenSource, refreshResult); logRequestFailure(requestEntry, "XSRF token cookie not found after automatic refresh."); throw new Error("XSRF token cookie not found after automatic refresh."); } refreshResult = await refreshXsrfCookie("missing-cookie"); refreshAttempted = true; continue; } const requestEntry = createTokenRelayRequestEntry(xsrfTokenSource, refreshResult); try { const response = await fetch(TOKEN_RELAY_URL, { method: "GET", credentials: "include", headers: { "x-xsrf-token": xsrfTokenSource.token } }); const responseBody = await readResponseBodyForDebug(response); if (!response.ok) { logRequestSuccess(requestEntry, response, responseBody); if (!refreshAttempted && isInvalidXsrfStatus(response.status)) { refreshResult = await refreshXsrfCookie(`tokenrelay-${response.status}`); refreshAttempted = true; requestEntry.metadata.xsrfRefresh = formatXsrfRefreshResult(refreshResult); renderDebugPanel(); continue; } throw new Error(`Token relay failed with status ${response.status}.`); } let responseData; try { responseData = await response.json(); } catch (error) { logRequestSuccess(requestEntry, response, responseBody); if (!refreshAttempted) { refreshResult = await refreshXsrfCookie("tokenrelay-invalid-response"); refreshAttempted = true; requestEntry.metadata.xsrfRefresh = formatXsrfRefreshResult(refreshResult); renderDebugPanel(); continue; } throw error; } if (!responseData || typeof responseData.access_token !== "string" || !responseData.access_token) { logRequestSuccess(requestEntry, response, responseBody); if (!refreshAttempted) { refreshResult = await refreshXsrfCookie("tokenrelay-missing-access-token"); refreshAttempted = true; requestEntry.metadata.xsrfRefresh = formatXsrfRefreshResult(refreshResult); renderDebugPanel(); continue; } throw new Error("Token relay response does not contain access_token."); } logRequestSuccess(requestEntry, response, createDebugBody(JSON.stringify({ ...responseData, access_token: maskToken(responseData.access_token) }, null, 2))); return responseData.access_token; } catch (error) { logRequestFailure(requestEntry, error.message || "Request failed."); throw error; } } } function createTokenRelayRequestEntry(xsrfTokenSource, refreshResult) { return logRequestStart({ name: "requestTokenRelay", method: "GET", url: TOKEN_RELAY_URL, headers: { "x-xsrf-token": xsrfTokenSource.token ? maskToken(xsrfTokenSource.token) : "(missing)" }, metadata: { xsrfCookieName: xsrfTokenSource.cookieName || "(missing)", xsrfTokenSource: xsrfTokenSource.source, xsrfMatchedCookies: xsrfTokenSource.matchedCookieNames && xsrfTokenSource.matchedCookieNames.length ? xsrfTokenSource.matchedCookieNames.join(", ") : "(none)", xsrfLookupDetails: xsrfTokenSource.lookupDetails && xsrfTokenSource.lookupDetails.length ? xsrfTokenSource.lookupDetails.join(" | ") : "(none)", xsrfTokenError: xsrfTokenSource.error || "", xsrfRefresh: formatXsrfRefreshResult(refreshResult) } }); } function isInvalidXsrfStatus(status) { return status === 400 || status === 401 || status === 403; } function formatXsrfRefreshResult(result) { if (!result) { return "not-requested"; } const outcome = result.refreshed ? "refreshed" : "not-refreshed"; const cookieName = result.cookieName || "(none)"; const duration = Number.isFinite(result.durationMs) ? `${result.durationMs}ms` : "unknown"; return `${result.reason}; ${outcome}; cookie=${cookieName}; duration=${duration}; ${result.error || ""}`.trim(); } async function requestOpportunitiesForPeriod(period) { const dateRange = getFiscalPeriodRange(period, new Date()); const requestVersion = ++periodRequestVersion; if (selectedOpportunityTypeValues.size === 0) { setOpportunitiesTableState({ items: [], status: "empty", message: "Select at least one Opportunity Type View in Preferences." }); return; } if (!dateRange) { setOpportunitiesTableState({ items: [], status: "empty", message: "No opportunities to display for this period." }); return; } try { const token = accessToken || await requestTokenRelayOnce(); if (requestVersion !== periodRequestVersion) { return; } await requestOpportunities(token, period, dateRange, requestVersion); } catch (error) { // Authentication and request errors are recorded by their respective request handlers. } } async function requestOpportunities(token, period, dateRange, requestVersion) { if (isCurrentPeriodRequest(requestVersion)) { currentProductsRequestId = ""; opptyProductsByNumber = new Map(); expandedProductOptyNumbers = new Set(); productsProgress = { state: "idle", completed: 0, total: 0, failed: 0, cacheHits: 0 }; productsLastCachedAt = 0; accountPlansRequestState = new Map(); renderProductsProgress(); setOpportunitiesTableState({ items: [], status: "loading", message: "Loading opportunities..." }); } try { const allItems = await requestOpportunitiesPage(token, period, dateRange, 0, [], requestVersion, 1); if (allItems && isCurrentPeriodRequest(requestVersion)) { setOpportunitiesTableState({ items: allItems, status: "ready", message: "" }); requestOpptyProductsInBackground(allItems, token); } } catch (error) { if (isCurrentPeriodRequest(requestVersion)) { setOpportunitiesTableState({ items: [], status: "error", message: "Unable to load opportunities." }); } throw error; } } async function requestOpportunitiesPage(token, period, dateRange, offset, accumulatedItems, requestVersion, page) { if (!isCurrentPeriodRequest(requestVersion)) { return null; } const payload = createOpportunitiesQueryPayload(dateRange, offset); const requestBody = JSON.stringify(payload); const requestEntry = logRequestStart({ name: "requestOpportunities", method: "POST", url: OPPORTUNITIES_QUERY_URL, headers: { Accept: "application/json", Authorization: `Bearer ${maskToken(token)}`, "Content-Type": "application/json", Origin: "https://eeho.fa.us2.oraclecloud.com", Preference: "transient" }, body: JSON.stringify(payload, null, 2), metadata: { period, page, offset, startCloseDate: dateRange.startCloseDate, endCloseDate: dateRange.endCloseDate } }); try { const response = await fetch(OPPORTUNITIES_QUERY_URL, { method: "POST", credentials: "include", headers: { Accept: "application/json", Authorization: `Bearer ${token}`, "Content-Type": "application/json", Origin: "https://eeho.fa.us2.oraclecloud.com", Preference: "transient" }, body: requestBody }); const responseBody = await readResponseBodyForDebug(response); logRequestSuccess(requestEntry, response, responseBody); if (!response.ok) { throw new Error(`Opportunities query failed with status ${response.status}.`); } const responseData = await response.json(); const pageItems = Array.isArray(responseData.items) ? responseData.items : []; const allItems = accumulatedItems.concat(pageItems); if (!responseData.hasMore) { return allItems; } const responseOffset = Number(responseData.offset); const responseCount = Number(responseData.count); const receivedCount = Number.isFinite(responseCount) ? responseCount : pageItems.length; const currentOffset = Number.isFinite(responseOffset) ? responseOffset : offset; if (receivedCount <= 0) { throw new Error("Opportunities query returned hasMore without additional results."); } return requestOpportunitiesPage( token, period, dateRange, currentOffset + receivedCount, allItems, requestVersion, page + 1 ); } catch (error) { logRequestFailure(requestEntry, error.message || "Request failed."); throw error; } } function requestOpptyProductsInBackground(opportunities, token) { const optyNumbers = Array.from(new Set(opportunities .map((item) => item && item.OptyNumber) .filter(Boolean) .map(String))); const requestId = `${Date.now()}-${Math.random().toString(36).slice(2)}`; currentProductsRequestId = requestId; opptyProductsByNumber = new Map(); expandedProductOptyNumbers = new Set(); productsProgress = { state: optyNumbers.length ? "running" : "complete", completed: 0, total: optyNumbers.length, failed: 0, cacheHits: 0 }; renderProductsProgress(); if (optyNumbers.length === 0) { return; } sendRuntimeMessage({ type: "opportunitiesExtension.requestOpptyProducts", requestId, accessToken: token, optyNumbers, cacheMaxAgeMs: opptyProductsCacheHours * 60 * 60 * 1000 }).then((response) => { if (response && response.ok === false && response.requestId === currentProductsRequestId && productsProgress.state === "running") { productsProgress.state = "error"; productsProgress.failed = productsProgress.total; renderProductsProgress(); } }).catch(() => { if (requestId === currentProductsRequestId && productsProgress.state === "running") { productsProgress.state = "error"; productsProgress.failed = productsProgress.total; renderProductsProgress(); } }); } function ensureProductsBackgroundMessageHandler() { if (window[PRODUCTS_MESSAGE_LISTENER_KEY]) { return; } const runtimeApi = typeof browser !== "undefined" ? browser : chrome; if (!runtimeApi || !runtimeApi.runtime || !runtimeApi.runtime.onMessage) { return; } window[PRODUCTS_MESSAGE_LISTENER_KEY] = true; runtimeApi.runtime.onMessage.addListener((message) => { if (!message || message.type !== "opportunitiesExtension.opptyProductsProgress" || message.requestId !== currentProductsRequestId) { return false; } if (message.optyNumber) { opptyProductsByNumber.set(String(message.optyNumber), { items: Array.isArray(message.items) ? message.items : [], fromCache: Boolean(message.fromCache), cachedAt: Number(message.cachedAt) || 0, error: message.error || "" }); } if (Number(message.cachedAt) > productsLastCachedAt) { productsLastCachedAt = Number(message.cachedAt); } productsProgress = { state: message.state || productsProgress.state, completed: Number(message.completed) || 0, total: Number(message.total) || productsProgress.total, failed: Number(message.failed) || 0, cacheHits: Number(message.cacheHits) || 0 }; renderProductsProgress(); queueProductsTableRender(); return false; }); } function sendRuntimeMessage(message) { const runtimeApi = typeof browser !== "undefined" ? browser : chrome; if (!runtimeApi || !runtimeApi.runtime || !runtimeApi.runtime.sendMessage) { return Promise.reject(new Error("Runtime messaging API unavailable.")); } if (typeof browser !== "undefined") { return runtimeApi.runtime.sendMessage(message); } return new Promise((resolve, reject) => { runtimeApi.runtime.sendMessage(message, (response) => { const lastError = runtimeApi.runtime.lastError; if (lastError) { reject(new Error(lastError.message)); return; } resolve(response); }); }); } function renderProductsProgress() { const badge = document.getElementById(PRODUCTS_PROGRESS_ID); if (!badge) { return; } badge.hidden = productsProgress.state === "idle"; badge.setAttribute("data-state", productsProgress.state); if (productsProgress.state === "running") { badge.textContent = `Products ${productsProgress.completed}/${productsProgress.total}`; badge.title = `${productsProgress.cacheHits} loaded from cache`; } else if (productsProgress.state === "complete") { badge.textContent = `Products ready ${productsProgress.total}`; badge.title = `${productsProgress.cacheHits} loaded from cache`; } else if (productsProgress.state === "complete-with-errors") { badge.textContent = `Products ${productsProgress.total - productsProgress.failed}/${productsProgress.total}`; badge.title = `${productsProgress.failed} product requests failed`; } else if (productsProgress.state === "error") { badge.textContent = "Products unavailable"; badge.title = "Unable to load opportunity products"; } else { badge.textContent = "Products ready 0"; } const timestamp = document.getElementById(PRODUCTS_CACHE_TIMESTAMP_ID); if (timestamp) { timestamp.hidden = !productsLastCachedAt; timestamp.textContent = productsLastCachedAt ? `Last cache: ${formatProductsCacheTimestamp(productsLastCachedAt)}` : ""; timestamp.title = productsLastCachedAt ? `Latest product cache: ${formatProductsCacheTimestamp(productsLastCachedAt)}` : ""; } } function formatProductsCacheTimestamp(timestamp) { try { return new Intl.DateTimeFormat("en-GB", { dateStyle: "short", timeStyle: "short" }).format(new Date(timestamp)); } catch (error) { return new Date(timestamp).toLocaleString(); } } function queueProductsTableRender() { if (productsRenderQueued) { return; } productsRenderQueued = true; window.requestAnimationFrame(() => { productsRenderQueued = false; renderOpportunitiesTable(); }); } function resetOpportunitiesTable() { selectedStages = new Set(STAGE_OPTIONS); selectedStatuses = new Set(STATUS_OPTIONS); selectedCustomerKeys = new Set(); customerSearch = ""; selectedOwnerKeys = new Set(); ownerSearch = ""; tableSearch = ""; hasSavedCustomerFilter = false; hasSavedOwnerFilter = false; customerFilterInitialized = false; ownerFilterInitialized = false; currentProductsRequestId = ""; opptyProductsByNumber = new Map(); expandedProductOptyNumbers = new Set(); productsProgress = { state: "idle", completed: 0, total: 0, failed: 0, cacheHits: 0 }; productsLastCachedAt = 0; accountPlansRequestState = new Map(); stageDashboardAmounts = new Map([["TOTAL", 0], ...STAGE_OPTIONS.map((stage) => [stage, 0])]); opportunitiesTableState = { items: [], status: "idle", message: "", sortKey: "", sortDirection: "ascending" }; } function resetCustomerAndOwnerFiltersForPeriod() { selectedCustomerKeys = new Set(); selectedOwnerKeys = new Set(); customerSearch = ""; ownerSearch = ""; hasSavedCustomerFilter = false; hasSavedOwnerFilter = false; customerFilterInitialized = false; ownerFilterInitialized = false; } function restoreFilterPreferences(select) { try { const rawPreferences = localStorage.getItem(FILTER_PREFERENCES_STORAGE_KEY); if (!rawPreferences) { return; } const preferences = JSON.parse(rawPreferences); const savedStages = Array.isArray(preferences.stages) ? preferences.stages.filter((stage) => STAGE_OPTIONS.includes(stage)) : null; const savedStatuses = Array.isArray(preferences.statuses) ? preferences.statuses.filter((status) => STATUS_OPTIONS.includes(status)) : null; if (savedStages) { selectedStages = new Set(savedStages); } if (savedStatuses) { selectedStatuses = new Set(savedStatuses); } if (Array.isArray(preferences.customerKeys)) { selectedCustomerKeys = new Set(preferences.customerKeys.map(String)); hasSavedCustomerFilter = true; customerFilterInitialized = true; } if (Array.isArray(preferences.ownerKeys)) { selectedOwnerKeys = new Set(preferences.ownerKeys.map(String)); hasSavedOwnerFilter = true; ownerFilterInitialized = true; } customerSearch = typeof preferences.customerSearch === "string" ? preferences.customerSearch : ""; ownerSearch = typeof preferences.ownerSearch === "string" ? preferences.ownerSearch : ""; tableSearch = typeof preferences.tableSearch === "string" ? preferences.tableSearch : ""; if (PERIOD_OPTIONS.includes(preferences.period)) { select.value = preferences.period; } } catch (error) { // Ignore malformed or unavailable browser storage and use default filters. } } function saveFilterPreferences(period) { try { const currentPeriod = period || document.querySelector(`#${MODAL_ID} select`)?.value || PERIOD_OPTIONS[0]; localStorage.setItem(FILTER_PREFERENCES_STORAGE_KEY, JSON.stringify({ period: currentPeriod, stages: Array.from(selectedStages), statuses: Array.from(selectedStatuses), customerKeys: hasSavedCustomerFilter ? Array.from(selectedCustomerKeys) : null, ownerKeys: hasSavedOwnerFilter ? Array.from(selectedOwnerKeys) : null, customerSearch, ownerSearch, tableSearch })); } catch (error) { // Ignore unavailable browser storage; filters still work for the current modal. } } function setOpportunitiesTableState(nextState) { if (nextState.status === "ready" && Array.isArray(nextState.items)) { resetCustomerFilter(nextState.items); resetOwnerFilter(nextState.items); } opportunitiesTableState = { ...opportunitiesTableState, ...nextState, sortKey: nextState.items ? "" : opportunitiesTableState.sortKey, sortDirection: nextState.items ? "ascending" : opportunitiesTableState.sortDirection }; renderOpportunitiesTable(); renderCustomerFilter(); renderOwnerFilter(); } function renderOpportunitiesTable() { const table = document.getElementById(OPPORTUNITIES_TABLE_ID); if (!table) { return; } table.textContent = ""; table.setAttribute("aria-busy", opportunitiesTableState.status === "loading" ? "true" : "false"); const caption = document.createElement("caption"); caption.className = "opportunities-extension-visually-hidden"; caption.textContent = "Opportunities"; const tableHead = document.createElement("thead"); const headerRow = document.createElement("tr"); OPPORTUNITIES_COLUMNS.forEach((column) => { const header = document.createElement("th"); const isSorted = opportunitiesTableState.sortKey === column.key; header.scope = "col"; if (column.sortable === false) { const label = document.createElement("span"); label.className = "opportunities-extension-table-header-label"; label.textContent = column.label; header.append(label); } else { header.setAttribute("aria-sort", isSorted ? opportunitiesTableState.sortDirection : "none"); const sortButton = document.createElement("button"); sortButton.type = "button"; sortButton.className = "opportunities-extension-sort-button"; sortButton.setAttribute("data-sort-direction", isSorted ? opportunitiesTableState.sortDirection : "none"); sortButton.setAttribute("aria-label", `Sort by ${column.label}${isSorted ? `, ${opportunitiesTableState.sortDirection}` : ""}`); sortButton.textContent = column.label; sortButton.addEventListener("click", () => sortOpportunitiesBy(column.key)); header.append(sortButton); } headerRow.append(header); }); tableHead.append(headerRow); const tableBody = document.createElement("tbody"); const items = getSortedOpportunities(); renderStageDashboard(items); if (opportunitiesTableState.status === "loading" || opportunitiesTableState.status === "error" || opportunitiesTableState.status === "empty" || (opportunitiesTableState.status === "ready" && items.length === 0)) { const row = document.createElement("tr"); const cell = document.createElement("td"); cell.className = "opportunities-extension-table-status"; cell.colSpan = OPPORTUNITIES_COLUMNS.length; cell.textContent = opportunitiesTableState.message || "No opportunities found."; row.append(cell); tableBody.append(row); } else { items.forEach((item) => { const row = document.createElement("tr"); OPPORTUNITIES_COLUMNS.forEach((column) => { const cell = document.createElement("td"); if (column.key === "stage") { cell.append(createStageBadge(column.display(item))); } else if (column.key === "status") { cell.append(createStatusBadge(column.display(item))); } else if (column.key === "actions") { cell.append(createCloudConsumptionButton(item)); } else if (column.key === "name" || column.key === "optyNumber") { const opportunityLink = createDetailLink( column.display(item), item.OptyNumber ? `${OPPORTUNITY_DETAIL_URL}${encodeURIComponent(item.OptyNumber)}` : "" ); if (column.key === "name" && item.OptyNumber) { const nameCellContent = document.createElement("span"); nameCellContent.className = "opportunities-extension-name-content"; nameCellContent.append(opportunityLink); const productsResult = opptyProductsByNumber.get(String(item.OptyNumber)); if (productsResult) { nameCellContent.append(createProductsCountButton(item.OptyNumber, productsResult)); } cell.append(nameCellContent); } else if (column.key === "optyNumber" && item.OptyNumber) { const opportunityCellContent = document.createElement("span"); opportunityCellContent.className = "opportunities-extension-opty-number-content"; opportunityCellContent.append(opportunityLink, createCopyOpportunityButton(item.OptyNumber)); cell.append(opportunityCellContent); } else { cell.append(opportunityLink); } } else if (column.key === "customer") { cell.append(createDetailLink( column.display(item), getNestedValue(item, ["CustomerAccount", "PartyId"]) ? `${ACCOUNT_DETAIL_URL}${encodeURIComponent(getNestedValue(item, ["CustomerAccount", "PartyId"]))}` : "" )); } else { cell.textContent = displayOpportunityValue(column, item); } row.append(cell); }); tableBody.append(row); if (item.OptyNumber && expandedProductOptyNumbers.has(String(item.OptyNumber))) { tableBody.append(createProductsDetailRow(item)); } }); } table.append(caption, tableHead, tableBody); } function renderStageDashboard(items) { const dashboard = document.getElementById(STAGE_DASHBOARD_ID); if (!dashboard) { return; } dashboard.textContent = ""; const totalCard = document.createElement("section"); const totalLabel = document.createElement("span"); const totalValue = document.createElement("strong"); totalCard.className = "opportunities-extension-stage-dashboard-card opportunities-extension-total-dashboard-card"; totalCard.setAttribute("data-stage", "TOTAL"); totalLabel.textContent = "Total Opportunities"; totalValue.className = "opportunities-extension-stage-dashboard-value"; totalValue.setAttribute("aria-label", "Total opportunities"); animateStageDashboardValue( totalValue, "TOTAL", items.length, (amount) => Math.round(amount).toLocaleString() ); totalCard.append(totalLabel, totalValue); dashboard.append(totalCard); STAGE_OPTIONS.forEach((stage) => { const stageItems = items.filter((item) => item.ForecastGroup_c === stage); const amount = stageItems.reduce((total, item) => { const value = Number(getNestedValue(item, ["PrimaryRevenue", "RevnAmount"])); return Number.isFinite(value) ? total + value : total; }, 0); const card = document.createElement("section"); const label = document.createElement("span"); const value = document.createElement("strong"); card.className = "opportunities-extension-stage-dashboard-card"; card.setAttribute("data-stage", stage); label.textContent = stage; value.className = "opportunities-extension-stage-dashboard-value"; value.setAttribute("aria-label", `${stage} total amount`); animateStageDashboardValue( value, stage, amount, createStageAmountFormatter(stageItems) ); card.append(label, value); dashboard.append(card); }); } function createStageAmountFormatter(items) { const currencies = new Set(items .map((item) => getNestedValue(item, ["PrimaryRevenue", "RevnAmountCurcyCode"])) .filter(Boolean)); if (currencies.size === 1) { const [currency] = currencies; try { const formatter = new Intl.NumberFormat(undefined, { style: "currency", currency, maximumFractionDigits: 0 }); return (amount) => formatter.format(amount); } catch (error) { // Fall through to a neutral numeric total when the currency code is invalid. } } return (amount) => amount.toLocaleString(undefined, { maximumFractionDigits: 0 }); } function animateStageDashboardValue(element, stage, target, format) { const current = stageDashboardAmounts.get(stage) || 0; stageDashboardAmounts.set(stage, target); if (current === target) { element.textContent = format(target); return; } const startTime = performance.now(); const duration = 480; function tick(now) { const progress = Math.min((now - startTime) / duration, 1); const easedProgress = 1 - Math.pow(1 - progress, 3); const value = current + ((target - current) * easedProgress); element.textContent = format(value); if (progress < 1) { requestAnimationFrame(tick); } else { element.textContent = format(target); } } requestAnimationFrame(tick); } function sortOpportunitiesBy(key) { const isSameColumn = opportunitiesTableState.sortKey === key; opportunitiesTableState.sortKey = key; opportunitiesTableState.sortDirection = isSameColumn && opportunitiesTableState.sortDirection === "ascending" ? "descending" : "ascending"; renderOpportunitiesTable(); } function renderStageFilterButtons() { const controls = document.getElementById(STAGE_FILTERS_ID); if (!controls) { return; } controls.textContent = ""; STAGE_OPTIONS.forEach((stage) => { const button = document.createElement("button"); const isPressed = selectedStages.has(stage); button.type = "button"; button.className = "opportunities-extension-stage-filter-button"; button.setAttribute("data-stage", stage); button.setAttribute("aria-pressed", isPressed ? "true" : "false"); button.textContent = stage; button.addEventListener("click", () => toggleStageFilter(stage)); controls.append(button); }); } function toggleStageFilter(stage) { if (selectedStages.has(stage)) { selectedStages.delete(stage); } else { selectedStages.add(stage); } saveFilterPreferences(); renderStageFilterButtons(); renderOpportunitiesTable(); } function renderStatusFilterButtons() { const controls = document.getElementById(STATUS_FILTERS_ID); if (!controls) { return; } controls.textContent = ""; STATUS_OPTIONS.forEach((status) => { const button = document.createElement("button"); const isPressed = selectedStatuses.has(status); button.type = "button"; button.className = "opportunities-extension-status-filter-button"; button.setAttribute("data-status", status); button.setAttribute("aria-pressed", isPressed ? "true" : "false"); button.textContent = status; button.addEventListener("click", () => toggleStatusFilter(status)); controls.append(button); }); } function toggleStatusFilter(status) { if (selectedStatuses.has(status)) { selectedStatuses.delete(status); } else { selectedStatuses.add(status); } saveFilterPreferences(); renderStatusFilterButtons(); renderOpportunitiesTable(); } function toggleCustomerFilterPanel() { const panel = document.getElementById(CUSTOMER_FILTER_PANEL_ID); const button = document.getElementById(CUSTOMER_FILTER_BUTTON_ID); if (!panel || !button || button.disabled) { return; } panel.hidden = !panel.hidden; button.setAttribute("aria-expanded", panel.hidden ? "false" : "true"); if (!panel.hidden) { const searchInput = document.getElementById(CUSTOMER_FILTER_SEARCH_ID); searchInput.focus(); } } function closeCustomerFilterPanel(restoreFocus = true) { const panel = document.getElementById(CUSTOMER_FILTER_PANEL_ID); const button = document.getElementById(CUSTOMER_FILTER_BUTTON_ID); if (!panel || panel.hidden) { return false; } panel.hidden = true; button.setAttribute("aria-expanded", "false"); if (restoreFocus) { button.focus(); } return true; } function toggleOwnerFilterPanel() { const panel = document.getElementById(OWNER_FILTER_PANEL_ID); const button = document.getElementById(OWNER_FILTER_BUTTON_ID); if (!panel || !button || button.disabled) { return; } panel.hidden = !panel.hidden; button.setAttribute("aria-expanded", panel.hidden ? "false" : "true"); if (!panel.hidden) { const searchInput = document.getElementById(OWNER_FILTER_SEARCH_ID); searchInput.focus(); } } function closeOwnerFilterPanel(restoreFocus = true) { const panel = document.getElementById(OWNER_FILTER_PANEL_ID); const button = document.getElementById(OWNER_FILTER_BUTTON_ID); if (!panel || panel.hidden) { return false; } panel.hidden = true; button.setAttribute("aria-expanded", "false"); if (restoreFocus) { button.focus(); } return true; } function resetCustomerFilter(items) { const customerOptions = getCustomerOptions(items); if (!customerFilterInitialized) { selectedCustomerKeys = new Set(customerOptions.map((customer) => customer.key)); customerFilterInitialized = true; } } function resetOwnerFilter(items) { const ownerOptions = getOwnerOptions(items); if (!ownerFilterInitialized) { selectedOwnerKeys = new Set(ownerOptions.map((owner) => owner.key)); ownerFilterInitialized = true; } } function renderCustomerFilter() { const trigger = document.getElementById(CUSTOMER_FILTER_BUTTON_ID); const searchInput = document.getElementById(CUSTOMER_FILTER_SEARCH_ID); const selectAllCheckbox = document.getElementById(CUSTOMER_FILTER_SELECT_ALL_ID); const customerOptions = getCustomerOptions(); if (!trigger || !searchInput || !selectAllCheckbox) { return; } const selectedCount = customerOptions.filter((customer) => selectedCustomerKeys.has(customer.key)).length; trigger.disabled = customerOptions.length === 0; trigger.textContent = getCustomerFilterSummary(customerOptions.length, selectedCount); searchInput.value = customerSearch; selectAllCheckbox.disabled = customerOptions.length === 0; selectAllCheckbox.checked = customerOptions.length > 0 && selectedCount === customerOptions.length; selectAllCheckbox.indeterminate = selectedCount > 0 && selectedCount < customerOptions.length; renderCustomerFilterList(); } function renderCustomerFilterList() { const list = document.getElementById(CUSTOMER_FILTER_LIST_ID); if (!list) { return; } const normalizedSearch = customerSearch.trim().toLocaleLowerCase(); const customerOptions = getCustomerOptions().filter((customer) => { return customer.label.toLocaleLowerCase().includes(normalizedSearch); }); list.textContent = ""; if (customerOptions.length === 0) { const empty = document.createElement("p"); empty.className = "opportunities-extension-customer-filter-empty"; empty.textContent = customerSearch ? "No matching customers." : "No customers available."; list.append(empty); return; } customerOptions.forEach((customer) => { const option = document.createElement("label"); option.className = "opportunities-extension-customer-filter-option"; const checkbox = document.createElement("input"); checkbox.type = "checkbox"; checkbox.checked = selectedCustomerKeys.has(customer.key); checkbox.addEventListener("change", () => { if (checkbox.checked) { selectedCustomerKeys.add(customer.key); } else { selectedCustomerKeys.delete(customer.key); } hasSavedCustomerFilter = true; saveFilterPreferences(); renderCustomerFilter(); renderOpportunitiesTable(); }); const label = document.createElement("span"); label.textContent = customer.label; option.append(checkbox, label); list.append(option); }); } function getCustomerOptions(items) { const optionsByKey = new Map(); (items || opportunitiesTableState.items).forEach((item) => { const key = getCustomerKey(item); const label = getNestedValue(item, ["CustomerAccount", "PartyUniqueName"]); if (key && label && !optionsByKey.has(key)) { optionsByKey.set(key, { key, label: String(label) }); } }); return Array.from(optionsByKey.values()).sort((first, second) => { return first.label.localeCompare(second.label, undefined, { sensitivity: "base" }); }); } function getCustomerKey(item) { const partyId = getNestedValue(item, ["CustomerAccount", "PartyId"]); const partyName = getNestedValue(item, ["CustomerAccount", "PartyUniqueName"]); if (partyId !== null && partyId !== undefined && partyId !== "") { return `party:${partyId}`; } return partyName ? `name:${partyName}` : ""; } function getOwnerName(item) { const owner = item.Owner; if (typeof owner === "string" || typeof owner === "number") { return owner; } if (owner && typeof owner === "object") { const firstName = owner.PersonFirstName; const lastName = owner.PersonLastName; if (firstName || lastName) { return [firstName, lastName].filter(Boolean).join(" "); } return owner.PartyName || owner.PartyUniqueName || owner.Name || owner.DisplayName || owner.ResourceName || owner.OwnerName || ""; } return ""; } function getCustomerFilterSummary(total, selected) { if (total === 0) { return "No customers"; } if (selected === total) { return "All customers"; } if (selected === 0) { return "No customers"; } return `${selected} customer${selected === 1 ? "" : "s"}`; } function renderOwnerFilter() { const trigger = document.getElementById(OWNER_FILTER_BUTTON_ID); const searchInput = document.getElementById(OWNER_FILTER_SEARCH_ID); const selectAllCheckbox = document.getElementById(OWNER_FILTER_SELECT_ALL_ID); const ownerOptions = getOwnerOptions(); if (!trigger || !searchInput || !selectAllCheckbox) { return; } const selectedCount = ownerOptions.filter((owner) => selectedOwnerKeys.has(owner.key)).length; trigger.disabled = ownerOptions.length === 0; trigger.textContent = getCustomerFilterSummary(ownerOptions.length, selectedCount) .replace("customer", "owner"); searchInput.value = ownerSearch; selectAllCheckbox.disabled = ownerOptions.length === 0; selectAllCheckbox.checked = ownerOptions.length > 0 && selectedCount === ownerOptions.length; selectAllCheckbox.indeterminate = selectedCount > 0 && selectedCount < ownerOptions.length; renderOwnerFilterList(); } function renderOwnerFilterList() { const list = document.getElementById(OWNER_FILTER_LIST_ID); if (!list) { return; } const normalizedSearch = ownerSearch.trim().toLocaleLowerCase(); const ownerOptions = getOwnerOptions().filter((owner) => { return owner.label.toLocaleLowerCase().includes(normalizedSearch); }); list.textContent = ""; if (ownerOptions.length === 0) { const empty = document.createElement("p"); empty.className = "opportunities-extension-customer-filter-empty"; empty.textContent = ownerSearch ? "No matching owners." : "No owners available."; list.append(empty); return; } ownerOptions.forEach((owner) => { const option = document.createElement("label"); option.className = "opportunities-extension-customer-filter-option"; const checkbox = document.createElement("input"); checkbox.type = "checkbox"; checkbox.checked = selectedOwnerKeys.has(owner.key); checkbox.addEventListener("change", () => { if (checkbox.checked) { selectedOwnerKeys.add(owner.key); } else { selectedOwnerKeys.delete(owner.key); } hasSavedOwnerFilter = true; saveFilterPreferences(); renderOwnerFilter(); renderOpportunitiesTable(); }); const label = document.createElement("span"); label.textContent = owner.label; option.append(checkbox, label); list.append(option); }); } function getOwnerOptions(items) { const optionsByKey = new Map(); (items || opportunitiesTableState.items).forEach((item) => { const key = getOwnerKey(item); const label = getOwnerName(item); if (key && label && !optionsByKey.has(key)) { optionsByKey.set(key, { key, label: String(label) }); } }); return Array.from(optionsByKey.values()).sort((first, second) => { return first.label.localeCompare(second.label, undefined, { sensitivity: "base" }); }); } function getOwnerKey(item) { const owner = item.Owner; const partyId = owner && typeof owner === "object" ? owner.PartyId : ""; const name = getOwnerName(item); if (partyId !== null && partyId !== undefined && partyId !== "") { return `party:${partyId}`; } return name ? `name:${name}` : ""; } function getSortedOpportunities() { const normalizedSearch = tableSearch.trim().toLocaleLowerCase(); const items = opportunitiesTableState.items.filter((item) => { return selectedStages.has(item.ForecastGroup_c) && matchesSelectedStatus(item.StatusCode) && selectedCustomerKeys.has(getCustomerKey(item)) && selectedOwnerKeys.has(getOwnerKey(item)) && matchesTableSearch(item, normalizedSearch); }); const column = OPPORTUNITIES_COLUMNS.find((candidate) => candidate.key === opportunitiesTableState.sortKey); if (!column) { return items; } const direction = opportunitiesTableState.sortDirection === "ascending" ? 1 : -1; return items.sort((first, second) => { const firstValue = column.value(first); const secondValue = column.value(second); const firstIsEmpty = firstValue === null || firstValue === undefined || firstValue === ""; const secondIsEmpty = secondValue === null || secondValue === undefined || secondValue === ""; if (firstIsEmpty || secondIsEmpty) { if (firstIsEmpty && secondIsEmpty) { return 0; } return firstIsEmpty ? 1 : -1; } const firstNumber = Number(firstValue); const secondNumber = Number(secondValue); if (Number.isFinite(firstNumber) && Number.isFinite(secondNumber)) { return (firstNumber - secondNumber) * direction; } return String(firstValue).localeCompare(String(secondValue), undefined, { numeric: true, sensitivity: "base" }) * direction; }); } function matchesTableSearch(item, normalizedSearch) { if (!normalizedSearch) { return true; } return OPPORTUNITIES_COLUMNS.some((column) => { const rawValue = stringifySearchValue(column.value(item)); const displayValue = stringifySearchValue(column.display(item)); return `${rawValue} ${displayValue}`.toLocaleLowerCase().includes(normalizedSearch); }); } function matchesSelectedStatus(statusCode) { if (statusCode === "WON_PENDING") { return selectedStatuses.has("WON"); } return selectedStatuses.has(statusCode); } function stringifySearchValue(value) { if (value === null || value === undefined) { return ""; } if (typeof value === "object") { return JSON.stringify(value); } return String(value); } function displayOpportunityValue(column, item) { const value = column.display(item); return value === null || value === undefined || value === "" ? "-" : String(value); } function createDetailLink(label, href) { if (label === null || label === undefined || label === "") { const placeholder = document.createElement("span"); placeholder.textContent = "-"; return placeholder; } if (!href) { const text = document.createElement("span"); text.textContent = String(label); return text; } const link = document.createElement("a"); link.href = href; link.target = "_blank"; link.rel = "noopener noreferrer"; link.textContent = String(label); return link; } function createCopyOpportunityButton(optyNumber) { const button = document.createElement("button"); const icon = document.createElementNS("http://www.w3.org/2000/svg", "svg"); const path = document.createElementNS("http://www.w3.org/2000/svg", "path"); button.type = "button"; button.className = "opportunities-extension-copy-button"; button.setAttribute("aria-label", `Copy opportunity number ${optyNumber}`); button.title = "Copy opportunity number"; button.addEventListener("click", (event) => { event.preventDefault(); event.stopPropagation(); copyOpportunityNumber(String(optyNumber)); }); icon.setAttribute("viewBox", "0 0 24 24"); icon.setAttribute("aria-hidden", "true"); path.setAttribute("d", "M8 8V5.5A2.5 2.5 0 0 1 10.5 3h8A2.5 2.5 0 0 1 21 5.5v8a2.5 2.5 0 0 1-2.5 2.5H16v2.5a2.5 2.5 0 0 1-2.5 2.5h-8A2.5 2.5 0 0 1 3 18.5v-8A2.5 2.5 0 0 1 5.5 8H8zm2 0h3.5A2.5 2.5 0 0 1 16 10.5v3.5h2.5a.5.5 0 0 0 .5-.5v-8a.5.5 0 0 0-.5-.5h-8a.5.5 0 0 0-.5.5V8zm3.5 2h-8a.5.5 0 0 0-.5.5v8a.5.5 0 0 0 .5.5h8a.5.5 0 0 0 .5-.5v-8a.5.5 0 0 0-.5-.5z"); icon.append(path); button.append(icon); return button; } function createCloudConsumptionButton(opportunity) { const button = document.createElement("button"); const optyNumber = String(opportunity.OptyNumber || ""); const partyNumber = getNestedValue(opportunity, ["CustomerAccount", "PartyNumber"]); const requestState = accountPlansRequestState.get(optyNumber); const isLoading = requestState && requestState.status === "loading"; button.type = "button"; button.className = "opportunities-extension-action-button"; button.setAttribute("aria-label", `View cloud consumption for ${optyNumber || "opportunity"}`); button.title = isLoading ? "Loading cloud consumption" : "View cloud consumption"; button.disabled = Boolean(isLoading); button.classList.toggle("opportunities-extension-action-button-loading", Boolean(isLoading)); button.append(createCloudConsumptionIcon()); button.addEventListener("click", () => { if (!partyNumber) { showCopyConfirmation("Customer Party Number is unavailable.", true); return; } accountPlansRequestState.set(optyNumber, { status: "loading" }); openAccountPlansModal(opportunity, [], "loading"); renderOpportunitiesTable(); requestConsumerTokenServiceOnce() .then((authorizations) => sendRuntimeMessage({ type: "opportunitiesExtension.requestAccountPlans", partyNumber: String(partyNumber), authorizations })) .then((response) => { appendBackgroundRequestLog(response && response.debugRequests); if (!response || !response.ok) { throw new Error(response && response.error ? response.error : "Unable to load cloud consumption."); } accountPlansRequestState.set(optyNumber, { status: "ready", plans: Array.isArray(response.plans) ? response.plans : [] }); renderOpportunitiesTable(); openAccountPlansModal(opportunity, response.plans || [], "ready"); }).catch((error) => { const errorMessage = error.message || "Unable to load cloud consumption."; accountPlansRequestState.set(optyNumber, { status: "error", error: errorMessage }); renderOpportunitiesTable(); openAccountPlansModal(opportunity, [], "error", errorMessage); showCopyConfirmation(errorMessage, true); }); }); return button; } function createCloudConsumptionIcon() { const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg"); const cloud = document.createElementNS("http://www.w3.org/2000/svg", "path"); const usage = document.createElementNS("http://www.w3.org/2000/svg", "path"); svg.setAttribute("viewBox", "0 0 24 24"); svg.setAttribute("aria-hidden", "true"); svg.setAttribute("focusable", "false"); cloud.setAttribute("d", "M7.2 18.5h10.1a4.2 4.2 0 0 0 .5-8.37A6.25 6.25 0 0 0 5.9 8.75a4.9 4.9 0 0 0 1.3 9.75Z"); usage.setAttribute("d", "M9 15v-2m3 2v-4m3 4V9"); [cloud, usage].forEach((path) => { path.setAttribute("fill", "none"); path.setAttribute("stroke", "currentColor"); path.setAttribute("stroke-linecap", "round"); path.setAttribute("stroke-linejoin", "round"); path.setAttribute("stroke-width", "1.7"); }); svg.append(cloud, usage); return svg; } function openAccountPlansModal(opportunity, plans, state = "ready", errorMessage = "") { const overlay = document.getElementById(MODAL_ID); const optyNumber = String(opportunity.OptyNumber || ""); const existingModal = document.getElementById(ACCOUNT_PLANS_MODAL_ID); if (!overlay) { return; } if (state !== "loading" && ( !existingModal || existingModal.hidden || existingModal.dataset.optyNumber !== optyNumber )) { return; } existingModal?.remove(); const modal = document.createElement("section"); modal.id = ACCOUNT_PLANS_MODAL_ID; modal.className = "opportunities-extension-account-plans-modal"; modal.dataset.optyNumber = optyNumber; modal.setAttribute("role", "dialog"); modal.setAttribute("aria-modal", "true"); modal.setAttribute("aria-labelledby", "opportunities-extension-account-plans-title"); modal.setAttribute("aria-busy", String(state === "loading")); const dialog = document.createElement("div"); dialog.className = "opportunities-extension-account-plans-dialog"; const header = document.createElement("header"); header.className = "opportunities-extension-account-plans-header"; const heading = document.createElement("div"); const title = document.createElement("h2"); title.id = "opportunities-extension-account-plans-title"; title.textContent = "Cloud Consumption"; const subtitle = document.createElement("p"); subtitle.textContent = getNestedValue(opportunity, ["CustomerAccount", "PartyUniqueName"]) || opportunity.Name || opportunity.OptyNumber || "Opportunity"; heading.append(title, subtitle); const closeButton = document.createElement("button"); closeButton.type = "button"; closeButton.className = "opportunities-extension-icon-button"; closeButton.setAttribute("aria-label", "Close cloud consumption"); closeButton.title = "Close"; closeButton.append(createCloseIcon()); closeButton.addEventListener("click", closeAccountPlansModal); header.append(heading, closeButton); const content = document.createElement("div"); content.className = "opportunities-extension-account-plans-content"; if (state === "loading") { content.append(createAccountPlansSkeleton()); } else if (state === "error") { const error = document.createElement("p"); error.className = "opportunities-extension-account-plans-error"; error.setAttribute("role", "alert"); error.textContent = errorMessage || "Unable to load cloud consumption."; content.append(error); } else if (!Array.isArray(plans) || plans.length === 0) { const empty = document.createElement("p"); empty.className = "opportunities-extension-account-plans-empty"; empty.textContent = "No account plans found for this customer."; content.append(empty); } else { const tabList = document.createElement("div"); tabList.className = "opportunities-extension-account-plans-tabs"; tabList.setAttribute("role", "tablist"); tabList.setAttribute("aria-label", "Account plans"); const panels = document.createElement("div"); panels.className = "opportunities-extension-account-plans-panels"; plans.forEach((plan, index) => { const tabId = `opportunities-extension-account-plan-tab-${index}`; const panelId = `opportunities-extension-account-plan-panel-${index}`; const tab = document.createElement("button"); tab.id = tabId; tab.type = "button"; tab.className = "opportunities-extension-account-plan-tab"; tab.setAttribute("role", "tab"); tab.setAttribute("aria-controls", panelId); tab.setAttribute("aria-selected", String(index === 0)); tab.tabIndex = index === 0 ? 0 : -1; const planLabel = [plan.planType, plan.subPlanNum] .filter((value) => value !== null && value !== undefined && value !== "") .join(" ") || `Plan ${index + 1}`; const headerStatus = plan.hdrStatusCode === null || plan.hdrStatusCode === undefined || plan.hdrStatusCode === "" ? "Unknown" : String(plan.hdrStatusCode); tab.setAttribute("aria-label", `${planLabel}. Header Status: ${headerStatus}`); tab.append(createAccountPlanStatusIndicator(headerStatus), document.createTextNode(planLabel)); const panel = document.createElement("section"); panel.id = panelId; panel.className = "opportunities-extension-account-plan-panel"; panel.setAttribute("role", "tabpanel"); panel.setAttribute("aria-labelledby", tabId); panel.hidden = index !== 0; panel.append(createAccountPlanDetails(plan)); tab.addEventListener("click", () => { tabList.querySelectorAll("[role='tab']").forEach((candidate) => { const isSelected = candidate === tab; candidate.setAttribute("aria-selected", String(isSelected)); candidate.tabIndex = isSelected ? 0 : -1; }); panels.querySelectorAll("[role='tabpanel']").forEach((candidate) => { candidate.hidden = candidate !== panel; }); }); tabList.append(tab); panels.append(panel); }); content.append(tabList, panels); } dialog.append(header, content); modal.append(dialog); overlay.append(modal); modal.querySelector("[role='tab'], .opportunities-extension-icon-button")?.focus(); } function createAccountPlansSkeleton() { const skeleton = document.createElement("div"); skeleton.className = "opportunities-extension-account-plans-skeleton"; skeleton.setAttribute("role", "status"); const announcement = document.createElement("span"); announcement.className = "opportunities-extension-visually-hidden"; announcement.textContent = "Loading cloud consumption"; const tabs = document.createElement("div"); tabs.className = "opportunities-extension-account-plans-skeleton-tabs"; [96, 124, 108].forEach((width) => { const tab = document.createElement("span"); tab.className = "opportunities-extension-account-plans-skeleton-bar"; tab.style.width = `${width}px`; tabs.append(tab); }); const details = document.createElement("div"); details.className = "opportunities-extension-account-plans-skeleton-details"; [72, 88, 64, 78, 58, 68].forEach((width) => { const row = document.createElement("div"); const label = document.createElement("span"); const value = document.createElement("span"); label.className = "opportunities-extension-account-plans-skeleton-bar opportunities-extension-account-plans-skeleton-label"; value.className = "opportunities-extension-account-plans-skeleton-bar opportunities-extension-account-plans-skeleton-value"; value.style.width = `${width}%`; row.append(label, value); details.append(row); }); skeleton.append(announcement, tabs, details); return skeleton; } function createAccountPlanStatusIndicator(statusValue) { const indicator = document.createElement("span"); const isActive = String(statusValue).toUpperCase() === "ACTIVE"; indicator.className = "opportunities-extension-account-plan-status-indicator"; indicator.dataset.active = String(isActive); indicator.setAttribute("aria-hidden", "true"); indicator.title = `Header Status: ${statusValue}`; return indicator; } function createAccountPlanDetails(plan) { const details = document.createElement("dl"); details.className = "opportunities-extension-account-plan-details"; const fields = [ { label: "Booking Customer", value: plan.bookingCustomer }, { label: "Sold To Party Name", value: plan.soldToPartyName }, { label: "Contract ID", value: plan.contractId }, { label: "Header Status", value: plan.hdrStatusCode }, { label: "Start Date", value: formatOracleResponseDate(plan.startDate) }, { label: "End Date", value: formatOracleResponseDate(plan.endDate) } ]; fields.forEach((field) => { const group = document.createElement("div"); const term = document.createElement("dt"); const description = document.createElement("dd"); term.textContent = field.label; description.textContent = field.value === null || field.value === undefined || field.value === "" ? "-" : String(field.value); group.append(term, description); details.append(group); }); return details; } function closeAccountPlansModal() { const modal = document.getElementById(ACCOUNT_PLANS_MODAL_ID); if (modal) { modal.hidden = true; } } function createProductsCountButton(optyNumber, productsResult) { const normalizedOptyNumber = String(optyNumber); const button = document.createElement("button"); const count = Array.isArray(productsResult.items) ? productsResult.items.length : 0; const isExpanded = expandedProductOptyNumbers.has(normalizedOptyNumber); button.type = "button"; button.className = "opportunities-extension-products-count"; button.textContent = String(count); button.setAttribute("aria-expanded", String(isExpanded)); button.setAttribute("aria-label", `${count} product${count === 1 ? "" : "s"} for opportunity ${normalizedOptyNumber}`); button.title = productsResult.error ? productsResult.error : `${count} product${count === 1 ? "" : "s"}`; button.classList.toggle("opportunities-extension-products-count-error", Boolean(productsResult.error)); button.addEventListener("click", (event) => { event.preventDefault(); event.stopPropagation(); if (isExpanded) { expandedProductOptyNumbers.delete(normalizedOptyNumber); } else { expandedProductOptyNumbers.add(normalizedOptyNumber); } renderOpportunitiesTable(); }); return button; } function createProductsDetailRow(opportunity) { const row = document.createElement("tr"); const cell = document.createElement("td"); const optyNumber = String(opportunity.OptyNumber); const productsResult = opptyProductsByNumber.get(optyNumber); const products = productsResult && Array.isArray(productsResult.items) ? productsResult.items : []; row.className = "opportunities-extension-products-detail-row"; cell.colSpan = OPPORTUNITIES_COLUMNS.length; cell.className = "opportunities-extension-products-detail-cell"; const panel = document.createElement("section"); panel.className = "opportunities-extension-products-detail"; const heading = document.createElement("div"); heading.className = "opportunities-extension-products-detail-heading"; const title = document.createElement("strong"); title.textContent = `Products for ${optyNumber}`; const summary = document.createElement("span"); summary.textContent = productsResult && productsResult.fromCache ? "Cached" : "Updated"; heading.append(title, summary); panel.append(heading); if (productsResult && productsResult.error) { const error = document.createElement("p"); error.className = "opportunities-extension-products-empty"; error.textContent = productsResult.error; panel.append(error); } else if (products.length === 0) { const empty = document.createElement("p"); empty.className = "opportunities-extension-products-empty"; empty.textContent = "No products found for this opportunity."; panel.append(empty); } else { const table = document.createElement("table"); table.className = "opportunities-extension-products-table"; table.setAttribute("aria-label", `Products for opportunity ${optyNumber}`); const columns = [ { label: "Product Group", value: (item) => item.ProdGroupName || "-" }, { label: "Workload", value: (item) => item.WorkloadName_c || "-" }, { label: "Currency", value: (item) => item.RevnAmountCurcyCode || "-" }, { label: "Amount", value: (item) => formatProductAmount(item.RevnAmount, item.RevnAmountCurcyCode) }, { label: "Type", value: (item) => item.TypeCode || "-" }, { label: "Status", value: (item) => item.StatusCode || "-" }, { label: "Win Probability", value: (item) => formatProductWinProbability(item.WinProb) }, { label: "Close date", value: (item) => formatOracleResponseDate(item.EffectiveDate) || "-" }, { label: "Consumption Start", value: (item) => formatOracleResponseDate(item.ConsumptionStartDate_c) || "-" }, { label: "Ramp Months", value: (item) => formatProductNumber(item.RampMonths_c) } ]; const head = document.createElement("thead"); const headerRow = document.createElement("tr"); columns.forEach((column) => { const header = document.createElement("th"); header.scope = "col"; header.textContent = column.label; headerRow.append(header); }); head.append(headerRow); const body = document.createElement("tbody"); products.forEach((product) => { const productRow = document.createElement("tr"); columns.forEach((column) => { const productCell = document.createElement("td"); productCell.textContent = String(column.value(product)); productRow.append(productCell); }); body.append(productRow); }); table.append(head, body); panel.append(table); } cell.append(panel); row.append(cell); return row; } function formatProductNumber(value) { const number = Number(value); return Number.isFinite(number) ? new Intl.NumberFormat("en-US", { maximumFractionDigits: 2 }).format(number) : "-"; } function formatProductAmount(value, currency) { const amount = Number(value); if (!Number.isFinite(amount)) { return "-"; } try { return new Intl.NumberFormat("en-US", { style: "currency", currency: currency || "USD" }).format(amount); } catch (error) { return `${currency || ""} ${formatProductNumber(amount)}`.trim(); } } function formatProductWinProbability(value) { const number = Number(value); return Number.isFinite(number) ? `${number}%` : "-"; } async function copyOpportunityNumber(optyNumber) { try { if (navigator.clipboard && navigator.clipboard.writeText) { await navigator.clipboard.writeText(optyNumber); } else { const input = document.createElement("textarea"); input.value = optyNumber; input.setAttribute("readonly", "true"); input.style.position = "fixed"; input.style.opacity = "0"; document.body.append(input); input.select(); document.execCommand("copy"); input.remove(); } showCopyConfirmation(`Opty Number ${optyNumber} copied.`); } catch (error) { showCopyConfirmation("Unable to copy opportunity number.", true); } } function showCopyConfirmation(message, isError = false) { const modal = document.getElementById(MODAL_ID); if (!modal) { return; } let toast = modal.querySelector(".opportunities-extension-copy-toast"); if (!toast) { toast = document.createElement("div"); toast.className = "opportunities-extension-copy-toast"; toast.setAttribute("role", "status"); modal.append(toast); } toast.classList.toggle("opportunities-extension-copy-toast-error", isError); toast.textContent = message; toast.hidden = false; window.clearTimeout(toast.copyTimeout); toast.copyTimeout = window.setTimeout(() => { toast.hidden = true; }, 2400); } function createStageBadge(stage) { const badge = document.createElement("span"); const normalizedStage = typeof stage === "string" ? stage.toUpperCase() : ""; badge.className = "opportunities-extension-stage-badge"; badge.setAttribute("data-stage", normalizedStage); badge.textContent = normalizedStage || "-"; return badge; } function createStatusBadge(status) { const badge = document.createElement("span"); const normalizedStatus = typeof status === "string" ? status.toUpperCase() : ""; badge.className = "opportunities-extension-status-badge"; badge.setAttribute("data-status", normalizedStatus); badge.textContent = normalizedStatus || "-"; return badge; } function getNestedValue(value, path) { return path.reduce((result, key) => result && result[key], value); } function formatWinProbability(item) { const value = getNestedValue(item, ["PrimaryRevenue", "WinProb"]); const number = Number(value); return Number.isFinite(number) ? `${number}%` : value; } function formatRevenue(item) { const amount = getNestedValue(item, ["PrimaryRevenue", "RevnAmount"]); const currency = getNestedValue(item, ["PrimaryRevenue", "RevnAmountCurcyCode"]); const number = Number(amount); if (!Number.isFinite(number)) { return amount; } if (!currency) { return number.toLocaleString(); } try { return new Intl.NumberFormat(undefined, { style: "currency", currency }).format(number); } catch (error) { return `${currency} ${number.toLocaleString()}`; } } function formatOracleResponseDate(value, includeTime) { if (typeof value !== "string") { return value; } const match = value.match(/^(\d{4})-(\d{2})-(\d{2})(?:T(\d{2}):(\d{2}))?/); if (!match) { return value; } const date = `${match[3]}/${match[2]}/${match[1]}`; return includeTime && match[4] ? `${date} ${match[4]}:${match[5]}` : date; } function isCurrentPeriodRequest(requestVersion) { return requestVersion === periodRequestVersion; } function createOpportunitiesQueryPayload(dateRange, offset) { return { aggregationResults: false, applications: ["ORACLE-ISS-APP"], onlyData: true, entity: "Opportunity", q: { op: "$and", criteria: [ { op: "$or", criteria: Array.from(selectedOpportunityTypeValues, (value) => ({ op: "$eq", attribute: "RecordSet", value })) }, { op: "$wi", attribute: "EffectiveDate", value1: dateRange.startCloseDate, value2: dateRange.endCloseDate, dynamicDate: false } ] }, keywords: null, keywordsFields: [ "Name", "OptyNumber", "PrimaryRevenue.WinProb", "CustomerAccount", "PrimaryRevenue.RevnAmount", "EffectiveDate", "StatusCode", "DealRisk_c", "Owner", "ForecastGroup_c", "LastUpdateDate", "PrimaryRevenue.RevnAmountCurcyCode", "OptyId" ], fields: [ "Name", "OptyNumber", "PrimaryRevenue.WinProb", "CustomerAccount", "PrimaryRevenue.RevnAmount", "EffectiveDate", "StatusCode", "DealRisk_c", "Owner", "ForecastGroup_c", "LastUpdateDate", "PrimaryRevenue.RevnAmountCurcyCode", "OptyId", "CustomerAccount.PartyUniqueName", "CustomerAccount.PartyId", "CustomerAccount.PartyNumber" ], sort: [], language: "en", skipInValidFields: true, skipHiddenFromUIFields: true, copiedFrom: "queries/d5dbcd01-9ff3-40be-9b5b-64744cbf7162", limit: OPPORTUNITIES_PAGE_LIMIT, offset }; } function getFiscalPeriodRange(period, currentDate) { const currentYear = currentDate.getFullYear(); const currentMonth = currentDate.getMonth(); const fiscalStartYear = currentMonth >= 5 ? currentYear : currentYear - 1; const currentQuarterIndex = Math.floor(((currentMonth - 5 + 12) % 12) / 3); if (period === "Current Fiscal Year") { const isBeforeFiscalYearStart = currentMonth < 5; const startYear = isBeforeFiscalYearStart ? currentYear - 1 : currentYear; const endYear = isBeforeFiscalYearStart ? currentYear : currentYear + 1; return createDateRange(startYear, 5, endYear, 4); } let quarterOffset; let numberOfQuarters = 1; if (period === "Current Quarter") { quarterOffset = currentQuarterIndex; } else if (period === "Next Quarter") { quarterOffset = currentQuarterIndex + 1; } else if (period === "Previous Quarter") { quarterOffset = currentQuarterIndex - 1; } else if (period === "4 Rolling Quarters (CQ + 3)") { quarterOffset = currentQuarterIndex; numberOfQuarters = 4; } else { return null; } const start = new Date(fiscalStartYear, 5 + (quarterOffset * 3), 1); const end = new Date(start.getFullYear(), start.getMonth() + (numberOfQuarters * 3), 0); return { startCloseDate: formatOracleDate(start), endCloseDate: formatOracleDate(end) }; } function createDateRange(startYear, startMonth, endYear, endMonth) { return { startCloseDate: formatOracleDate(new Date(startYear, startMonth, 1)), endCloseDate: formatOracleDate(new Date(endYear, endMonth + 1, 0)) }; } function formatOracleDate(date) { 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}T00:00:00.000Z`; } function logRequestStart(details) { const entry = { id: requestLog.length + 1, name: details.name, method: details.method, url: details.url, headers: details.headers, status: "pending", startedAt: new Date(), completedAt: null, durationMs: null, httpStatus: null, statusText: "", requestHeaders: details.headers, requestBody: details.body || "", metadata: details.metadata || {}, responseHeaders: {}, responseBody: "", responseBodyTruncated: false, error: "" }; requestLog.push(entry); renderDebugPanel(); return entry; } function logRequestSuccess(entry, response, responseBody) { entry.status = response.ok ? "success" : "failed"; entry.completedAt = new Date(); entry.durationMs = entry.completedAt.getTime() - entry.startedAt.getTime(); entry.httpStatus = response.status; entry.statusText = response.statusText || ""; entry.responseHeaders = headersToObject(response.headers); entry.responseBody = responseBody.value; entry.responseBodyTruncated = responseBody.truncated; renderDebugPanel(); } function logRequestFailure(entry, errorMessage) { entry.status = "failed"; entry.completedAt = new Date(); entry.durationMs = entry.completedAt.getTime() - entry.startedAt.getTime(); entry.error = errorMessage; renderDebugPanel(); } function appendBackgroundRequestLog(entries) { if (!Array.isArray(entries) || entries.length === 0) { return; } entries.forEach((entry) => { requestLog.push({ id: requestLog.length + 1, name: entry.name || "backgroundRequest", method: entry.method || "GET", url: entry.url || "", headers: entry.requestHeaders || {}, status: entry.status || "failed", startedAt: new Date(entry.startedAt), completedAt: entry.completedAt ? new Date(entry.completedAt) : null, durationMs: entry.durationMs, httpStatus: entry.httpStatus, statusText: entry.statusText || "", requestHeaders: entry.requestHeaders || {}, requestBody: entry.requestBody || "", metadata: entry.metadata || { executionContext: "background" }, responseHeaders: entry.responseHeaders || {}, responseBody: entry.responseBody || "", responseBodyTruncated: Boolean(entry.responseBodyTruncated), error: entry.error || "" }); }); renderDebugPanel(); } async function readResponseBodyForDebug(response) { try { const text = await response.clone().text(); return createDebugBody(text); } catch (error) { return { value: `Unable to read response body: ${error.message || "unknown error"}`, truncated: false }; } } function createDebugBody(text) { if (text.length > DEBUG_BODY_LIMIT) { return { value: `${text.slice(0, DEBUG_BODY_LIMIT)}\n... truncated ${text.length - DEBUG_BODY_LIMIT} characters`, truncated: true }; } return { value: text || "(empty response body)", truncated: false }; } function headersToObject(headers) { const headerMap = {}; headers.forEach((value, key) => { headerMap[key] = value; }); return headerMap; } function formatHeaderBlock(headers) { const entries = Object.entries(headers); if (entries.length === 0) { return "(none)"; } return entries .map(([key, value]) => `${key}: ${debugSecretsVisible || !isSensitiveDebugKey(key) ? value : formatHiddenDebugValue(value)}`) .join("\n"); } function formatDebugBody(body) { const text = body || "(none)"; if (debugSecretsVisible) { return text; } try { return JSON.stringify(maskSensitiveDebugValue(JSON.parse(text)), null, 2); } catch (error) { return text; } } function maskSensitiveDebugValue(value, parentKey = "") { if (Array.isArray(value)) { return value.map((item) => maskSensitiveDebugValue(item, parentKey)); } if (!value || typeof value !== "object") { return value; } return Object.fromEntries(Object.entries(value).map(([key, child]) => { const isSensitive = isSensitiveDebugKey(key) || (key === "value" && /uiaasHeader|uiassHeader/i.test(parentKey)); return [key, isSensitive ? formatHiddenDebugValue(child) : maskSensitiveDebugValue(child, key)]; })); } function isSensitiveDebugKey(key) { return /authorization|cookie|password|secret|(^|[_-])token($|[_-])|token$/i.test(key); } function formatHiddenDebugValue(value) { if (value === null || value === undefined || value === "") { return "(missing)"; } return `(present, ${String(value).length} characters)`; } function formatMetadataBlock(metadata) { const entries = Object.entries(metadata); if (entries.length === 0) { return "(none)"; } return entries .map(([key, value]) => `${key}: ${value || "(none)"}`) .join("\n"); } function maskToken(token) { if (token.length <= 10) { return "(present)"; } return `${token.slice(0, 4)}...${token.slice(-4)}`; } function toggleDebugPanel() { const modal = document.getElementById(MODAL_ID); if (!modal || modal.hidden) { return; } const panel = document.getElementById(DEBUG_PANEL_ID); if (!panel) { return; } panel.hidden = !panel.hidden; renderDebugPanel(); } function renderDebugPanel() { const panel = document.getElementById(DEBUG_PANEL_ID); if (!panel || panel.hidden) { return; } panel.textContent = ""; const panelHeader = document.createElement("header"); panelHeader.className = "opportunities-extension-debug-panel-header"; const heading = document.createElement("div"); const title = document.createElement("h2"); title.textContent = "Debug Requests"; const summary = document.createElement("p"); summary.textContent = `${requestLog.length} request${requestLog.length === 1 ? "" : "s"} since modal opened.`; heading.append(title, summary); const visibilityButton = document.createElement("button"); visibilityButton.type = "button"; visibilityButton.className = "opportunities-extension-icon-button opportunities-extension-debug-visibility-button"; visibilityButton.setAttribute("aria-pressed", String(debugSecretsVisible)); visibilityButton.setAttribute("aria-label", debugSecretsVisible ? "Hide sensitive values" : "Show sensitive values"); visibilityButton.title = debugSecretsVisible ? "Hide sensitive values" : "Show sensitive values"; visibilityButton.append(createDebugVisibilityIcon(debugSecretsVisible)); visibilityButton.addEventListener("click", () => { debugSecretsVisible = !debugSecretsVisible; renderDebugPanel(); }); panelHeader.append(heading, visibilityButton); const list = document.createElement("div"); list.className = "opportunities-extension-debug-list"; if (requestLog.length === 0) { const empty = document.createElement("div"); empty.className = "opportunities-extension-debug-empty"; empty.textContent = "No requests recorded yet."; list.append(empty); } requestLog.forEach((entry) => { const item = document.createElement("details"); item.className = `opportunities-extension-debug-item opportunities-extension-debug-item-${entry.status}`; const heading = document.createElement("summary"); heading.className = "opportunities-extension-debug-heading"; const name = document.createElement("strong"); name.textContent = `${entry.id}. ${entry.name}`; const httpStatus = document.createElement("span"); httpStatus.className = "opportunities-extension-debug-http-status"; httpStatus.textContent = entry.httpStatus ? String(entry.httpStatus) : "pending"; const status = document.createElement("span"); status.className = "opportunities-extension-debug-status"; status.textContent = entry.status; const summaryMeta = document.createElement("span"); summaryMeta.className = "opportunities-extension-debug-summary-meta"; summaryMeta.append(httpStatus, status); heading.append(name, summaryMeta); const lines = [ `${entry.method} ${entry.url}`, `Started: ${formatDebugTime(entry.startedAt)}`, entry.completedAt ? `Completed: ${formatDebugTime(entry.completedAt)} (${entry.durationMs}ms)` : "Completed: pending", entry.httpStatus ? `HTTP: ${entry.httpStatus} ${entry.statusText}`.trim() : "", entry.error ? `Error: ${entry.error}` : "", `Response body truncated: ${entry.responseBodyTruncated ? "yes" : "no"}` ].filter(Boolean); const details = document.createElement("div"); details.className = "opportunities-extension-debug-details"; appendDebugBlock(details, "Request", lines.join("\n")); appendDebugBlock(details, "Request Metadata", formatMetadataBlock(entry.metadata)); appendDebugBlock(details, "Request Headers", formatHeaderBlock(entry.requestHeaders)); appendDebugBlock(details, "Request Body", formatDebugBody(entry.requestBody)); appendDebugBlock(details, "Response Headers", formatHeaderBlock(entry.responseHeaders)); appendDebugBlock(details, "Response Body", formatDebugBody(entry.responseBody)); item.append(heading, details); list.append(item); }); panel.append(panelHeader, list); } function createDebugVisibilityIcon(secretsVisible) { const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg"); const eye = document.createElementNS("http://www.w3.org/2000/svg", "path"); const pupil = document.createElementNS("http://www.w3.org/2000/svg", "circle"); svg.setAttribute("viewBox", "0 0 24 24"); svg.setAttribute("aria-hidden", "true"); svg.setAttribute("focusable", "false"); eye.setAttribute("d", "M2.5 12s3.5-6.5 9.5-6.5 9.5 6.5 9.5 6.5-3.5 6.5-9.5 6.5S2.5 12 2.5 12Z"); pupil.setAttribute("cx", "12"); pupil.setAttribute("cy", "12"); pupil.setAttribute("r", "2.75"); [eye, pupil].forEach((shape) => { shape.setAttribute("fill", "none"); shape.setAttribute("stroke", "currentColor"); shape.setAttribute("stroke-linecap", "round"); shape.setAttribute("stroke-linejoin", "round"); shape.setAttribute("stroke-width", "1.7"); }); svg.append(eye, pupil); if (secretsVisible) { const slash = document.createElementNS("http://www.w3.org/2000/svg", "path"); slash.setAttribute("d", "M4 4l16 16"); slash.setAttribute("fill", "none"); slash.setAttribute("stroke", "currentColor"); slash.setAttribute("stroke-linecap", "round"); slash.setAttribute("stroke-width", "1.9"); svg.append(slash); } return svg; } function appendDebugBlock(container, label, value) { const block = document.createElement("section"); const heading = document.createElement("h3"); const content = document.createElement("pre"); heading.textContent = label; content.textContent = value; block.append(heading, content); container.append(block); } function formatDebugTime(date) { return date.toLocaleTimeString(undefined, { hour: "2-digit", minute: "2-digit", second: "2-digit" }); } async function getXsrfToken() { const extensionCookie = await getXsrfTokenFromExtensionCookies(); if (extensionCookie.token) { return extensionCookie; } const xsrfCookie = document.cookie .split(";") .map((cookie) => cookie.trim()) .find((cookie) => cookie.startsWith("XSRF-TOKEN-")); if (!xsrfCookie) { return { token: "", cookieName: "", source: extensionCookie.error ? "document.cookie fallback after cookies API error" : "document.cookie fallback", matchedCookieNames: extensionCookie.matchedCookieNames || [], lookupDetails: extensionCookie.lookupDetails || [], error: extensionCookie.error || "" }; } const cookieName = xsrfCookie.slice(0, xsrfCookie.indexOf("=")); const tokenValue = xsrfCookie.slice(xsrfCookie.indexOf("=") + 1); return { token: decodeURIComponent(tokenValue), cookieName, source: "document.cookie", matchedCookieNames: extensionCookie.matchedCookieNames || [], lookupDetails: extensionCookie.lookupDetails || [], error: "" }; } function getXsrfTokenFromExtensionCookies() { const runtimeApi = typeof browser !== "undefined" ? browser : chrome; if (!runtimeApi || !runtimeApi.runtime || !runtimeApi.runtime.sendMessage) { return Promise.resolve({ token: "", cookieName: "", source: "unavailable cookies API", error: "Runtime messaging API unavailable." }); } return new Promise((resolve) => { runtimeApi.runtime.sendMessage({ type: "opportunitiesExtension.getXsrfToken" }, (response) => { const lastError = runtimeApi.runtime.lastError; if (lastError) { resolve({ token: "", cookieName: "", source: "cookies API", matchedCookieNames: [], lookupDetails: [], error: lastError.message }); return; } resolve({ token: response && response.ok ? response.token : "", cookieName: response && response.ok ? response.cookieName : "", source: "cookies API", matchedCookieNames: response && response.matchedCookieNames ? response.matchedCookieNames : [], lookupDetails: response && response.lookupDetails ? response.lookupDetails : [], error: response && response.error ? response.error : "" }); }); }); } function refreshXsrfCookie(reason) { const runtimeApi = typeof browser !== "undefined" ? browser : chrome; if (!runtimeApi || !runtimeApi.runtime || !runtimeApi.runtime.sendMessage) { return Promise.resolve({ ok: false, refreshed: false, cookieName: "", durationMs: 0, reason, error: "Runtime messaging API unavailable." }); } return new Promise((resolve) => { runtimeApi.runtime.sendMessage({ type: "opportunitiesExtension.refreshXsrfCookie" }, (response) => { const lastError = runtimeApi.runtime.lastError; if (lastError) { resolve({ ok: false, refreshed: false, cookieName: "", durationMs: 0, reason, error: lastError.message }); return; } resolve({ ...(response || { ok: false, refreshed: false, cookieName: "", durationMs: 0, error: "Empty refresh response." }), reason }); }); }); } function updateAuthBadge(status) { const badge = document.querySelector(`#${MODAL_ID} .opportunities-extension-auth-badge`); if (!badge) { return; } const badgeStatus = status === AUTH_STATUS.authenticated || status === AUTH_STATUS.unauthenticated ? status : AUTH_STATUS.pending; badge.setAttribute("data-auth-status", badgeStatus); badge.className = `opportunities-extension-auth-badge opportunities-extension-auth-badge-${badgeStatus}`; badge.textContent = badgeStatus === AUTH_STATUS.authenticated ? "Authenticated" : badgeStatus === AUTH_STATUS.unauthenticated ? "Unauthenticated" : "Authenticating"; } function closeOpportunitiesModal() { const modal = document.getElementById(MODAL_ID); if (modal) { closePreferencesModal(); closeAccountPlansModal(); modal.hidden = true; } debugSecretsVisible = false; document.documentElement.classList.remove("opportunities-extension-scroll-lock"); } function getSavedTheme() { try { return localStorage.getItem(THEME_STORAGE_KEY) === "dark" ? "dark" : "light"; } catch (error) { return "light"; } } function applyTheme(modal, button, theme) { const isDarkTheme = theme === "dark"; modal.setAttribute("data-theme", isDarkTheme ? "dark" : "light"); button.textContent = ""; button.setAttribute("aria-label", isDarkTheme ? "Ativar modo claro" : "Ativar modo escuro"); button.title = isDarkTheme ? "Light mode" : "Dark mode"; button.append(createThemeIcon(isDarkTheme ? "sun" : "moon")); try { localStorage.setItem(THEME_STORAGE_KEY, isDarkTheme ? "dark" : "light"); } catch (error) { // Continue with the selected theme when browser storage is unavailable. } } function createThemeIcon(iconName) { const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg"); const path = document.createElementNS("http://www.w3.org/2000/svg", "path"); svg.setAttribute("viewBox", "0 0 24 24"); svg.setAttribute("aria-hidden", "true"); svg.setAttribute("focusable", "false"); if (iconName === "sun") { path.setAttribute("d", "M12 3V1m0 22v-2m9-9h2M1 12h2m15.36 6.36 1.42 1.42M4.22 4.22l1.42 1.42m12.72 0 1.42-1.42M4.22 19.78l1.42-1.42M16.5 12a4.5 4.5 0 1 1-9 0 4.5 4.5 0 0 1 9 0Z"); } else { path.setAttribute("d", "M20.5 14.2A8.5 8.5 0 0 1 9.8 3.5a8.5 8.5 0 1 0 10.7 10.7Z"); } path.setAttribute("fill", "none"); path.setAttribute("stroke", "currentColor"); path.setAttribute("stroke-linecap", "round"); path.setAttribute("stroke-linejoin", "round"); path.setAttribute("stroke-width", "1.8"); svg.append(path); return svg; } function createSettingsIcon() { const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg"); const upperControl = document.createElementNS("http://www.w3.org/2000/svg", "circle"); const lowerControl = document.createElementNS("http://www.w3.org/2000/svg", "circle"); svg.setAttribute("viewBox", "0 0 24 24"); svg.setAttribute("class", "opportunities-extension-settings-icon"); svg.setAttribute("aria-hidden", "true"); svg.setAttribute("focusable", "false"); appendPath(svg, "", "M4 7h16M4 17h16"); [upperControl, lowerControl].forEach((control, index) => { control.setAttribute("cx", index === 0 ? "9" : "15"); control.setAttribute("cy", index === 0 ? "7" : "17"); control.setAttribute("r", "2.5"); control.setAttribute("fill", "currentColor"); control.setAttribute("stroke", "currentColor"); control.setAttribute("stroke-width", "1.8"); }); svg.querySelector("path").setAttribute("fill", "none"); svg.querySelector("path").setAttribute("stroke", "currentColor"); svg.querySelector("path").setAttribute("stroke-linecap", "round"); svg.querySelector("path").setAttribute("stroke-width", "1.8"); svg.append(upperControl, lowerControl); return svg; } function createCloseIcon() { const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg"); const path = document.createElementNS("http://www.w3.org/2000/svg", "path"); svg.setAttribute("viewBox", "0 0 24 24"); svg.setAttribute("aria-hidden", "true"); svg.setAttribute("focusable", "false"); path.setAttribute("d", "m6 6 12 12M18 6 6 18"); path.setAttribute("fill", "none"); path.setAttribute("stroke", "currentColor"); path.setAttribute("stroke-linecap", "round"); path.setAttribute("stroke-width", "1.8"); svg.append(path); return svg; } function getSavedOpportunityTypeValues() { try { const rawPreferences = localStorage.getItem(OPPORTUNITY_TYPE_PREFERENCES_STORAGE_KEY); if (!rawPreferences) { return DEFAULT_OPPORTUNITY_TYPE_VALUES; } const preferences = JSON.parse(rawPreferences); const values = Array.isArray(preferences.opportunityTypeValues) ? preferences.opportunityTypeValues : null; if (!values) { return DEFAULT_OPPORTUNITY_TYPE_VALUES; } const allowedValues = new Set(OPPORTUNITY_TYPE_OPTIONS.map((option) => option.value)); return values.filter((value) => allowedValues.has(value)); } catch (error) { return DEFAULT_OPPORTUNITY_TYPE_VALUES; } } function getSavedOpptyProductsCacheHours() { try { const rawPreferences = localStorage.getItem(OPPORTUNITY_TYPE_PREFERENCES_STORAGE_KEY); if (!rawPreferences) { return DEFAULT_OPPTY_PRODUCTS_CACHE_HOURS; } const preferences = JSON.parse(rawPreferences); const cacheHours = Number(preferences.opptyProductsCacheHours); return OPPTY_PRODUCTS_CACHE_OPTIONS.some((option) => option.value === cacheHours) ? cacheHours : DEFAULT_OPPTY_PRODUCTS_CACHE_HOURS; } catch (error) { return DEFAULT_OPPTY_PRODUCTS_CACHE_HOURS; } } function openPreferencesModal() { const overlay = document.getElementById(MODAL_ID); if (!overlay) { return; } const existingModal = document.getElementById(PREFERENCES_MODAL_ID); if (existingModal) { const savedValues = new Set(getSavedOpportunityTypeValues()); existingModal.querySelectorAll("input[name='opportunityTypeView']").forEach((checkbox) => { checkbox.checked = savedValues.has(checkbox.value); }); const cacheSelect = existingModal.querySelector("select[name='opptyProductsCacheHours']"); if (cacheSelect) { cacheSelect.value = String(getSavedOpptyProductsCacheHours()); } existingModal.hidden = false; existingModal.querySelector("input")?.focus(); return; } const preferencesModal = document.createElement("section"); preferencesModal.id = PREFERENCES_MODAL_ID; preferencesModal.className = "opportunities-extension-preferences-modal"; preferencesModal.setAttribute("role", "dialog"); preferencesModal.setAttribute("aria-modal", "true"); preferencesModal.setAttribute("aria-labelledby", "opportunities-extension-preferences-title"); const dialog = document.createElement("div"); dialog.className = "opportunities-extension-preferences-dialog"; const header = document.createElement("header"); header.className = "opportunities-extension-preferences-header"; const title = document.createElement("h2"); title.id = "opportunities-extension-preferences-title"; title.textContent = "Preferences"; const closeButton = document.createElement("button"); closeButton.type = "button"; closeButton.className = "opportunities-extension-icon-button"; closeButton.setAttribute("aria-label", "Fechar preferências"); closeButton.title = "Close"; closeButton.append(createCloseIcon()); closeButton.addEventListener("click", closePreferencesModal); header.append(title, closeButton); const content = document.createElement("div"); content.className = "opportunities-extension-preferences-content"; const fieldset = document.createElement("fieldset"); fieldset.className = "opportunities-extension-preferences-fieldset"; const legend = document.createElement("legend"); legend.textContent = "Opportunity Type View"; fieldset.append(legend); const selectedValues = new Set(getSavedOpportunityTypeValues()); const options = document.createElement("div"); options.className = "opportunities-extension-preferences-options"; OPPORTUNITY_TYPE_OPTIONS.forEach((option) => { const label = document.createElement("label"); label.className = "opportunities-extension-preferences-option"; const checkbox = document.createElement("input"); checkbox.type = "checkbox"; checkbox.name = "opportunityTypeView"; checkbox.value = option.value; checkbox.checked = selectedValues.has(option.value); const text = document.createElement("span"); text.textContent = option.label; label.append(checkbox, text); options.append(label); }); fieldset.append(options); const cacheField = document.createElement("label"); cacheField.className = "opportunities-extension-preferences-cache-field"; const cacheLabel = document.createElement("span"); cacheLabel.textContent = "Product cache duration"; const cacheSelect = document.createElement("select"); cacheSelect.name = "opptyProductsCacheHours"; cacheSelect.setAttribute("aria-label", "Product cache duration"); OPPTY_PRODUCTS_CACHE_OPTIONS.forEach((option) => { const optionElement = document.createElement("option"); optionElement.value = String(option.value); optionElement.textContent = option.label; optionElement.selected = option.value === getSavedOpptyProductsCacheHours(); cacheSelect.append(optionElement); }); cacheField.append(cacheLabel, cacheSelect); content.append(fieldset, cacheField); const footer = document.createElement("footer"); footer.className = "opportunities-extension-preferences-footer"; const saveButton = document.createElement("button"); saveButton.type = "button"; saveButton.className = "opportunities-extension-button opportunities-extension-button-primary"; saveButton.textContent = "Save"; saveButton.addEventListener("click", () => { const selectedValues = Array.from(preferencesModal.querySelectorAll("input[name='opportunityTypeView']:checked"), (checkbox) => checkbox.value); const selectedCacheHours = Number(preferencesModal.querySelector("select[name='opptyProductsCacheHours']")?.value); selectedOpportunityTypeValues = new Set(selectedValues); opptyProductsCacheHours = OPPTY_PRODUCTS_CACHE_OPTIONS.some((option) => option.value === selectedCacheHours) ? selectedCacheHours : DEFAULT_OPPTY_PRODUCTS_CACHE_HOURS; try { localStorage.setItem(OPPORTUNITY_TYPE_PREFERENCES_STORAGE_KEY, JSON.stringify({ opportunityTypeValues: selectedValues, opptyProductsCacheHours })); } catch (error) { // Continue with the saved selection for the active modal when storage is unavailable. } closePreferencesModal(); const periodSelect = overlay.querySelector("select[name='opportunityPeriod']"); requestOpportunitiesForPeriod(periodSelect?.value || PERIOD_OPTIONS[0]); }); footer.append(saveButton); dialog.append(header, content, footer); preferencesModal.append(dialog); overlay.append(preferencesModal); preferencesModal.querySelector("input")?.focus(); } function closePreferencesModal() { const preferencesModal = document.getElementById(PREFERENCES_MODAL_ID); if (preferencesModal) { preferencesModal.hidden = true; } } function ensureEscapeKeyHandler() { if (window[ESCAPE_LISTENER_KEY]) { return; } window[ESCAPE_LISTENER_KEY] = true; document.addEventListener("keydown", (event) => { const modal = document.getElementById(MODAL_ID); if (!modal || modal.hidden) { return; } if (event.ctrlKey && event.key.toLowerCase() === "d") { event.preventDefault(); event.stopPropagation(); toggleDebugPanel(); return; } if (event.key === "Escape") { const accountPlansModal = document.getElementById(ACCOUNT_PLANS_MODAL_ID); if (accountPlansModal && !accountPlansModal.hidden) { closeAccountPlansModal(); event.preventDefault(); return; } const preferencesModal = document.getElementById(PREFERENCES_MODAL_ID); if (preferencesModal && !preferencesModal.hidden) { closePreferencesModal(); event.preventDefault(); return; } if (closeCustomerFilterPanel()) { event.preventDefault(); return; } if (closeOwnerFilterPanel()) { event.preventDefault(); return; } closeOpportunitiesModal(); } }); document.addEventListener("pointerdown", (event) => { const modal = document.getElementById(MODAL_ID); const panel = document.getElementById(CUSTOMER_FILTER_PANEL_ID); const ownerPanel = document.getElementById(OWNER_FILTER_PANEL_ID); if (!modal || modal.hidden) { return; } if (panel && !panel.hidden) { const customerFilter = panel.closest(".opportunities-extension-customer-filter"); if (customerFilter && !customerFilter.contains(event.target)) { closeCustomerFilterPanel(false); } } if (ownerPanel && !ownerPanel.hidden) { const ownerFilter = ownerPanel.closest(".opportunities-extension-customer-filter"); if (ownerFilter && !ownerFilter.contains(event.target)) { closeOwnerFilterPanel(false); } } }); } function ensureExtensionStyles() { if (document.getElementById(STYLE_ID)) { return; } const style = document.createElement("style"); style.id = STYLE_ID; style.textContent = ` .opportunities-extension-scroll-lock { overflow: hidden !important; } .opportunities-extension-modal, .opportunities-extension-modal * { box-sizing: border-box; font-family: "Oracle Sans", Arial, Helvetica, sans-serif; } .opportunities-extension-modal { position: fixed; inset: 0; z-index: 2147483647; background: #f5f4f2; color: #000000; } .opportunities-extension-modal[hidden] { display: none !important; } .opportunities-extension-shell { height: 100vh; min-height: 0; display: grid; grid-template-rows: auto minmax(0, 1fr); background: #f5f4f2; } .opportunities-extension-header { position: relative; display: flex; align-items: flex-start; justify-content: space-between; gap: 24px; padding: 28px 40px 22px; border-bottom: 1px solid #dedbd7; background: linear-gradient(90deg, #8f624a 0 11%, #c74634 11% 23%, #6f5a7f 23% 36%, #00758f 36% 51%, #d4b06a 51% 63%, transparent 63% 100%) top left / 100% 6px no-repeat, #ffffff; } .opportunities-extension-header::before { position: absolute; left: 40px; bottom: -1px; width: 64px; height: 3px; background: #00758f; content: ""; } .opportunities-extension-header h1 { margin: 0; color: #000000; font-size: 24px; font-weight: 700; line-height: 1.2; letter-spacing: 0; } .opportunities-extension-header p { margin: 0; color: #5f5a55; font-size: 14px; line-height: 1.4; } .opportunities-extension-subtitle-row { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; margin-top: 6px; } .opportunities-extension-auth-badge { display: inline-flex; align-items: center; min-height: 22px; padding: 2px 8px; border-radius: 999px; color: #ffffff; font-size: 12px; font-weight: 700; line-height: 1.2; } .opportunities-extension-auth-badge-authenticated { background: #3f6f17; } .opportunities-extension-auth-badge-unauthenticated { background: #c5331f; } .opportunities-extension-auth-badge-pending { background: #6f5a7f; } .opportunities-extension-products-progress { display: inline-flex; align-items: center; min-height: 22px; padding: 2px 8px; border-radius: 999px; background: #e8f4f6; color: #006d7a; font-size: 12px; font-weight: 700; line-height: 1.2; } .opportunities-extension-products-progress[hidden] { display: none !important; } .opportunities-extension-products-cache-timestamp { display: inline-flex; align-items: center; min-height: 22px; padding: 2px 8px; border-radius: 999px; background: #f0eeeb; color: #5f5a55; font-size: 11px; font-weight: 600; line-height: 1.2; } .opportunities-extension-products-cache-timestamp[hidden] { display: none !important; } .opportunities-extension-products-progress[data-state="running"]::before { width: 12px; height: 12px; margin-right: 6px; border: 2px solid currentColor; border-right-color: transparent; border-radius: 50%; content: ""; animation: opportunities-extension-spin .8s linear infinite; } .opportunities-extension-products-progress[data-state="complete"] { background: #e2f2e5; color: #356d19; } .opportunities-extension-products-progress[data-state="complete-with-errors"], .opportunities-extension-products-progress[data-state="error"] { background: #f9e3e1; color: #a52b1c; } @keyframes opportunities-extension-spin { to { transform: rotate(360deg); } } .opportunities-extension-icon-button { width: 36px; height: 36px; flex: 0 0 36px; border: 1px solid transparent; border-radius: 4px; background: transparent; color: #312d2a; font-size: 18px; font-weight: 600; line-height: 1; cursor: pointer; } .opportunities-extension-header-actions { display: inline-flex; align-items: center; gap: 6px; } .opportunities-extension-icon-button svg { width: 18px; height: 18px; vertical-align: middle; } .opportunities-extension-icon-button .opportunities-extension-settings-icon { width: 20px; height: 20px; } .opportunities-extension-icon-button:hover, .opportunities-extension-icon-button:focus { border-color: #b8b2ad; background: #f5f4f2; outline: none; } .opportunities-extension-body { display: grid; min-height: 0; grid-template-rows: auto auto minmax(0, 1fr); gap: 16px; padding: 22px 40px 36px; background: #f5f4f2; overflow: hidden; } .opportunities-extension-form { display: flex; align-items: end; flex-wrap: wrap; gap: 24px; min-height: 82px; max-width: none; margin: 0; padding: 14px 16px; border: 1px solid #dedbd7; border-radius: 4px; background: #ffffff; } .opportunities-extension-field { display: grid; max-width: max-content; min-width: 270px; width: max-content; gap: 6px; color: #312d2a; font-size: 13px; font-weight: 600; line-height: 1.3; } .opportunities-extension-stage-dashboard { display: grid; grid-template-columns: repeat(6, minmax(0, 1fr)); gap: 12px; } .opportunities-extension-stage-dashboard-card { display: grid; gap: 6px; min-width: 0; padding: 12px 16px; border: 1px solid #dedbd7; border-top: 3px solid #5f5a55; border-radius: 4px; background: #ffffff; } .opportunities-extension-stage-dashboard-card span { color: #5f5a55; font-size: 11px; font-weight: 700; line-height: 1.2; } .opportunities-extension-stage-dashboard-value { overflow: hidden; color: #312d2a; font-size: 22px; font-weight: 700; line-height: 1.2; text-overflow: ellipsis; white-space: nowrap; } .opportunities-extension-stage-dashboard-card[data-stage="SQL"] { border-top-color: #008aa6; } .opportunities-extension-total-dashboard-card { border-top-color: #312d2a; } .opportunities-extension-stage-dashboard-card[data-stage="PIPELINE"] { border-top-color: #6f6863; } .opportunities-extension-stage-dashboard-card[data-stage="UPSIDE"] { border-top-color: #c87b00; } .opportunities-extension-stage-dashboard-card[data-stage="FORECAST"] { border-top-color: #755b8b; } .opportunities-extension-stage-dashboard-card[data-stage="WON"] { border-top-color: #4b7f22; } .opportunities-extension-stage-filter { display: grid; grid-template-rows: auto 44px; gap: 6px; min-width: 0; } .opportunities-extension-stage-filter-label { color: #312d2a; font-size: 13px; font-weight: 600; line-height: 1.3; } .opportunities-extension-stage-filter-controls { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; } .opportunities-extension-stage-filter-button { min-height: 36px; padding: 6px 12px; border: 1px solid currentColor; border-radius: 18px; background: #ffffff; color: #5f5a55; font-size: 12px; font-weight: 700; letter-spacing: 0; line-height: 1.2; cursor: pointer; } .opportunities-extension-stage-filter-button[data-stage="SQL"] { color: #006b84; } .opportunities-extension-stage-filter-button[data-stage="PIPELINE"] { color: #5f5a55; } .opportunities-extension-stage-filter-button[data-stage="UPSIDE"] { color: #945400; } .opportunities-extension-stage-filter-button[data-stage="FORECAST"] { color: #624e74; } .opportunities-extension-stage-filter-button[data-stage="WON"] { color: #3f6f17; } .opportunities-extension-stage-filter-button[aria-pressed="true"][data-stage="SQL"] { border-color: #006b84; background: #006b84; color: #ffffff; } .opportunities-extension-stage-filter-button[aria-pressed="true"][data-stage="PIPELINE"] { border-color: #5f5a55; background: #5f5a55; color: #ffffff; } .opportunities-extension-stage-filter-button[aria-pressed="true"][data-stage="UPSIDE"] { border-color: #945400; background: #945400; color: #ffffff; } .opportunities-extension-stage-filter-button[aria-pressed="true"][data-stage="FORECAST"] { border-color: #624e74; background: #624e74; color: #ffffff; } .opportunities-extension-stage-filter-button[aria-pressed="true"][data-stage="WON"] { border-color: #3f6f17; background: #3f6f17; color: #ffffff; } .opportunities-extension-stage-filter-button:focus-visible { outline: 2px solid #00758f; outline-offset: 2px; } .opportunities-extension-status-filter { display: grid; grid-template-rows: auto 44px; gap: 6px; min-width: 0; } .opportunities-extension-status-filter-label { color: #312d2a; font-size: 13px; font-weight: 600; line-height: 1.3; } .opportunities-extension-status-filter-controls { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; } .opportunities-extension-status-filter-button { min-height: 36px; padding: 6px 12px; border: 1px solid currentColor; border-radius: 18px; background: #ffffff; font-size: 12px; font-weight: 700; letter-spacing: 0; line-height: 1.2; cursor: pointer; } .opportunities-extension-status-filter-button[data-status="OPEN"] { color: #197a3d; } .opportunities-extension-status-filter-button[data-status="CLOSED"] { color: #4b5563; } .opportunities-extension-status-filter-button[data-status="LOST"] { color: #b13b34; } .opportunities-extension-status-filter-button[data-status="WON"] { color: #1f5f99; } .opportunities-extension-status-filter-button[aria-pressed="true"][data-status="OPEN"] { border-color: #197a3d; background: #197a3d; color: #ffffff; } .opportunities-extension-status-filter-button[aria-pressed="true"][data-status="CLOSED"] { border-color: #4b5563; background: #4b5563; color: #ffffff; } .opportunities-extension-status-filter-button[aria-pressed="true"][data-status="LOST"] { border-color: #b13b34; background: #b13b34; color: #ffffff; } .opportunities-extension-status-filter-button[aria-pressed="true"][data-status="WON"] { border-color: #1f5f99; background: #1f5f99; color: #ffffff; } .opportunities-extension-status-filter-button:focus-visible { outline: 2px solid #00758f; outline-offset: 2px; } .opportunities-extension-customer-filter { position: relative; display: grid; grid-template-rows: auto 44px; gap: 6px; min-width: 220px; } .opportunities-extension-customer-filter-label { color: #312d2a; font-size: 13px; font-weight: 600; line-height: 1.3; } .opportunities-extension-customer-filter-trigger { position: relative; min-width: 220px; min-height: 44px; padding: 10px 38px 10px 12px; border: 1px solid #b8b2ad; border-radius: 3px; background: #ffffff; color: #312d2a; font-size: 14px; line-height: 1.3; text-align: left; cursor: pointer; } .opportunities-extension-customer-filter-trigger::after { position: absolute; top: 50%; right: 16px; width: 0; height: 0; border-top: 6px solid #312d2a; border-right: 5px solid transparent; border-left: 5px solid transparent; content: ""; pointer-events: none; transform: translateY(-35%); } .opportunities-extension-customer-filter-trigger:disabled { cursor: not-allowed; background: #f5f4f2; color: #7d7772; } .opportunities-extension-customer-filter-trigger:focus-visible { border-color: #312d2a; box-shadow: 0 0 0 1px #312d2a; outline: none; } .opportunities-extension-customer-filter-panel { position: absolute; top: calc(100% + 8px); left: 0; z-index: 4; display: grid; width: min(360px, calc(100vw - 80px)); gap: 10px; padding: 12px; border: 1px solid #8f8a85; border-radius: 4px; background: #ffffff; box-shadow: 0 4px 12px rgba(0, 0, 0, .18); } .opportunities-extension-customer-filter-panel[hidden] { display: none !important; } .opportunities-extension-customer-filter-search { width: 100%; min-height: 40px; padding: 8px 10px; border: 1px solid #b8b2ad; border-radius: 3px; background: #ffffff; color: #312d2a; font-size: 14px; } .opportunities-extension-customer-filter-search:focus { border-color: #312d2a; box-shadow: 0 0 0 1px #312d2a; outline: none; } .opportunities-extension-customer-filter-select-all, .opportunities-extension-customer-filter-option { display: flex; align-items: center; gap: 8px; color: #312d2a; font-size: 13px; line-height: 1.35; } .opportunities-extension-customer-filter-select-all { min-height: 32px; padding-bottom: 8px; border-bottom: 1px solid #dedbd7; font-weight: 700; } .opportunities-extension-customer-filter-panel input[type="checkbox"] { width: 16px; height: 16px; flex: 0 0 16px; accent-color: #00758f; } .opportunities-extension-customer-filter-list { display: block; max-height: 250px; overflow: auto; } .opportunities-extension-customer-filter-option { display: grid; grid-template-columns: 16px minmax(0, 1fr); align-items: start; min-height: 0; height: auto !important; padding: 8px 4px; cursor: pointer; } .opportunities-extension-customer-filter-option input[type="checkbox"] { margin-top: 1px; } .opportunities-extension-customer-filter-option span { display: block; min-width: 0; line-height: 18px; overflow-wrap: anywhere; white-space: normal; } .opportunities-extension-customer-filter-option:hover { background: #f5f4f2; } .opportunities-extension-customer-filter-empty { margin: 4px 0; color: #5f5a55; font-size: 13px; } .opportunities-extension-table-search-field { display: grid; grid-template-rows: auto 44px; gap: 6px; width: 260px; color: #312d2a; font-size: 13px; font-weight: 600; line-height: 1.3; } .opportunities-extension-table-search-field input { width: 100%; min-height: 44px; padding: 10px 12px; border: 1px solid #b8b2ad; border-radius: 3px; background: #ffffff; color: #312d2a; font-size: 14px; line-height: 1.3; } .opportunities-extension-table-search-field input:focus { border-color: #312d2a; box-shadow: 0 0 0 1px #312d2a; outline: none; } .opportunities-extension-select-wrap { position: relative; display: block; width: 270px; max-width: 100%; } .opportunities-extension-select-wrap::after { position: absolute; top: 50%; right: 16px; width: 0; height: 0; border-left: 5px solid transparent; border-right: 5px solid transparent; border-top: 6px solid #000000; content: ""; pointer-events: none; transform: translateY(-35%); } .opportunities-extension-field select { width: 100%; min-width: 0; max-width: 100%; min-height: 44px; box-sizing: border-box; appearance: none; border: 1px solid #b8b2ad; border-radius: 3px; background: #ffffff; color: #000000; font-size: 14px; line-height: 1.3; padding: 11px 44px 11px 12px; } .opportunities-extension-field select:focus { border-color: #312d2a; box-shadow: 0 0 0 1px #312d2a; outline: none; } .opportunities-extension-table-surface { min-height: 0; height: 100%; border: 1px solid #dedbd7; border-radius: 4px; background: #ffffff; overflow-x: auto; overflow-y: auto; } .opportunities-extension-table { width: 100%; min-width: 0; border-collapse: collapse; table-layout: fixed; color: #312d2a; font-size: 13px; line-height: 1.35; } .opportunities-extension-table thead { background: #faf9f8; } .opportunities-extension-table th { position: sticky; top: 0; z-index: 1; height: 44px; border-bottom: 1px solid #b8b2ad; background: #faf9f8; color: #312d2a; font-size: 12px; font-weight: 700; text-align: left; white-space: nowrap; } .opportunities-extension-table td { min-height: 48px; padding: 12px 16px; border-bottom: 1px solid #ebe8e5; vertical-align: middle; overflow-wrap: anywhere; } .opportunities-extension-table th:nth-child(1) { width: 16%; } .opportunities-extension-table th:nth-child(2) { width: 8%; } .opportunities-extension-table th:nth-child(3) { width: 18%; } .opportunities-extension-table th:nth-child(4) { width: 11%; } .opportunities-extension-table th:nth-child(5) { width: 8%; } .opportunities-extension-table th:nth-child(6) { width: 8%; } .opportunities-extension-table th:nth-child(7) { width: 7%; } .opportunities-extension-table th:nth-child(8) { width: 6%; } .opportunities-extension-table th:nth-child(9) { width: 7%; } .opportunities-extension-table th:nth-child(10) { width: 7%; } .opportunities-extension-table th:nth-child(11) { width: 4%; } .opportunities-extension-table td:nth-child(2), .opportunities-extension-table td:nth-child(5), .opportunities-extension-table td:nth-child(6), .opportunities-extension-table td:nth-child(8) { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .opportunities-extension-table td:nth-child(9) { padding-right: 6px; padding-left: 6px; white-space: nowrap; overflow: visible; } .opportunities-extension-table td:nth-child(7) { padding-right: 6px; padding-left: 6px; white-space: nowrap; overflow: visible; } .opportunities-extension-table td:nth-child(10) { padding-right: 6px; padding-left: 6px; font-size: 12px; white-space: nowrap; } .opportunities-extension-table td:nth-child(11) { padding-right: 6px; padding-left: 6px; text-align: center; white-space: nowrap; } .opportunities-extension-table tbody tr:hover { background: #f7f6f4; } .opportunities-extension-table tbody tr:last-child td { border-bottom: 0; } .opportunities-extension-table a { color: #006b84; text-decoration: underline; text-decoration-thickness: 1px; text-underline-offset: 2px; } .opportunities-extension-table a:hover { color: #004f63; } .opportunities-extension-opty-number-content { display: inline-flex; align-items: center; gap: 6px; max-width: 100%; } .opportunities-extension-name-content { display: flex; align-items: center; gap: 8px; min-width: 0; } .opportunities-extension-name-content > a { min-width: 0; } .opportunities-extension-products-count { display: inline-flex; align-items: center; justify-content: center; flex: 0 0 auto; min-width: 24px; min-height: 22px; padding: 2px 7px; border: 1px solid #7ca7b2; border-radius: 999px; background: #e8f4f6; color: #006d7a; font-size: 11px; font-weight: 700; line-height: 1; cursor: pointer; } .opportunities-extension-products-count:hover, .opportunities-extension-products-count:focus-visible, .opportunities-extension-products-count[aria-expanded="true"] { border-color: #00758f; background: #00758f; color: #ffffff; outline: none; } .opportunities-extension-products-count-error { border-color: #d8887e; background: #f9e3e1; color: #a52b1c; } .opportunities-extension-products-detail-row:hover { background: transparent !important; } .opportunities-extension-products-detail-cell { padding: 0 !important; } .opportunities-extension-products-detail { padding: 14px 20px 18px; border-bottom: 1px solid #c9c5c1; background: #f0eeeb; } .opportunities-extension-products-detail-heading { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-bottom: 10px; color: #312d2a; } .opportunities-extension-products-detail-heading strong { font-size: 13px; } .opportunities-extension-products-detail-heading span { color: #5f5a55; font-size: 11px; font-weight: 600; text-transform: uppercase; } .opportunities-extension-products-table { width: 100%; table-layout: fixed; border-collapse: collapse; border: 1px solid #dedbd7; background: #ffffff; font-size: 12px; } .opportunities-extension-products-table th, .opportunities-extension-products-table td { position: static; height: auto; padding: 8px 10px; border-bottom: 1px solid #ebe8e5; background: transparent; overflow-wrap: anywhere; vertical-align: middle; white-space: normal; } .opportunities-extension-products-table th { background: #faf9f8; font-size: 11px; } .opportunities-extension-products-table th:nth-child(1) { width: 16%; } .opportunities-extension-products-table th:nth-child(2) { width: 16%; } .opportunities-extension-products-table th:nth-child(3) { width: 8%; } .opportunities-extension-products-table th:nth-child(4) { width: 10%; } .opportunities-extension-products-table th:nth-child(5) { width: 8%; } .opportunities-extension-products-table th:nth-child(6) { width: 8%; } .opportunities-extension-products-table th:nth-child(7) { width: 7%; } .opportunities-extension-products-table th:nth-child(8) { width: 10%; } .opportunities-extension-products-table th:nth-child(9) { width: 12%; } .opportunities-extension-products-table th:nth-child(10) { width: 5%; } .opportunities-extension-products-empty { margin: 0; padding: 10px 0; color: #5f5a55; font-size: 13px; } .opportunities-extension-copy-button { display: inline-flex; align-items: center; justify-content: center; flex: 0 0 auto; width: 24px; height: 24px; padding: 3px; border: 0; border-radius: 3px; background: transparent; color: #00758f; cursor: pointer; } .opportunities-extension-copy-button svg { width: 16px; height: 16px; fill: currentColor; } .opportunities-extension-copy-button:hover, .opportunities-extension-copy-button:focus-visible { background: #e8f4f6; color: #004f63; outline: none; } .opportunities-extension-action-button { display: inline-flex; align-items: center; justify-content: center; width: 32px; height: 32px; padding: 5px; border: 1px solid transparent; border-radius: 4px; background: transparent; color: #00758f; cursor: pointer; } .opportunities-extension-action-button svg { width: 21px; height: 21px; } .opportunities-extension-action-button:hover, .opportunities-extension-action-button:focus-visible { border-color: #7ca7b2; background: #e8f4f6; color: #004f63; outline: none; } .opportunities-extension-action-button:disabled { cursor: wait; opacity: .72; } .opportunities-extension-action-button-loading svg { animation: opportunities-extension-pulse 1s ease-in-out infinite; } @keyframes opportunities-extension-pulse { 50% { opacity: .35; } } .opportunities-extension-copy-toast { position: fixed; right: 28px; bottom: 28px; z-index: 3; max-width: min(360px, calc(100vw - 56px)); padding: 12px 16px; border-radius: 4px; background: #2f6f18; box-shadow: 0 4px 12px rgba(0, 0, 0, .18); color: #ffffff; font-size: 13px; font-weight: 700; } .opportunities-extension-copy-toast-error { background: #a52b1c; } .opportunities-extension-copy-toast[hidden] { display: none !important; } .opportunities-extension-stage-badge { display: inline-flex; align-items: center; min-height: 24px; padding: 3px 8px; border-radius: 12px; background: #ebe8e5; color: #312d2a; font-size: 11px; font-weight: 700; line-height: 1.2; white-space: nowrap; } .opportunities-extension-stage-badge[data-stage="SQL"] { background: #d9f0f5; color: #006b84; } .opportunities-extension-stage-badge[data-stage="PIPELINE"] { background: #ebe8e5; color: #5f5a55; } .opportunities-extension-stage-badge[data-stage="UPSIDE"] { background: #fff0d8; color: #945400; } .opportunities-extension-stage-badge[data-stage="FORECAST"] { background: #eee8f5; color: #624e74; } .opportunities-extension-stage-badge[data-stage="WON"] { background: #e5f1d9; color: #3f6f17; } .opportunities-extension-status-badge { display: inline-flex; align-items: center; min-height: 24px; padding: 3px 8px; border-radius: 12px; background: #ebe8e5; color: #312d2a; font-size: 11px; font-weight: 700; line-height: 1.2; white-space: nowrap; } .opportunities-extension-status-badge[data-status="OPEN"] { background: #e2f2e5; color: #197a3d; } .opportunities-extension-status-badge[data-status="CLOSED"] { background: #e5e7eb; color: #4b5563; } .opportunities-extension-status-badge[data-status="LOST"] { background: #f9e3e1; color: #b13b34; } .opportunities-extension-status-badge[data-status="WON"] { background: #e3edf8; color: #1f5f99; } .opportunities-extension-sort-button { position: relative; display: inline-flex; align-items: center; width: 100%; min-height: 44px; padding: 8px 42px 8px 16px; border: 0; background: transparent; color: inherit; font: inherit; font-weight: inherit; letter-spacing: 0; line-height: 1.2; text-align: left; white-space: normal; cursor: pointer; } .opportunities-extension-table-header-label { display: inline-flex; align-items: center; min-height: 44px; padding: 8px 6px; } .opportunities-extension-sort-button::after { position: absolute; right: 15px; color: #5f5a55; content: "↕"; font-size: 16px; font-weight: 400; } .opportunities-extension-sort-button[data-sort-direction="ascending"]::after { color: #00758f; content: "↑"; } .opportunities-extension-sort-button[data-sort-direction="descending"]::after { color: #00758f; content: "↓"; } .opportunities-extension-sort-button:hover { background: #f0eeeb; } .opportunities-extension-sort-button:focus-visible { outline: 2px solid #00758f; outline-offset: -2px; } .opportunities-extension-table-status { height: 152px; color: #5f5a55; font-size: 14px; text-align: center; } .opportunities-extension-visually-hidden { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0; } .opportunities-extension-preferences-modal { position: absolute; inset: 0; z-index: 3; display: grid; place-items: center; padding: 24px; background: rgba(0, 0, 0, .42); } .opportunities-extension-preferences-modal[hidden] { display: none !important; } .opportunities-extension-account-plans-modal { position: absolute; inset: 0; z-index: 4; display: grid; place-items: center; padding: 24px; background: rgba(0, 0, 0, .42); } .opportunities-extension-account-plans-modal[hidden] { display: none !important; } .opportunities-extension-account-plans-dialog { display: grid; width: min(860px, 100%); max-height: min(720px, calc(100vh - 48px)); grid-template-rows: auto minmax(0, 1fr); overflow: hidden; border: 1px solid #8f8a85; border-radius: 4px; background: #ffffff; box-shadow: 0 8px 24px rgba(0, 0, 0, .24); color: #312d2a; } .opportunities-extension-account-plans-header { display: flex; align-items: flex-start; justify-content: space-between; gap: 16px; padding: 18px 20px; border-bottom: 1px solid #dedbd7; } .opportunities-extension-account-plans-header h2 { margin: 0; font-size: 20px; line-height: 1.2; } .opportunities-extension-account-plans-header p { margin: 5px 0 0; color: #5f5a55; font-size: 13px; line-height: 1.35; } .opportunities-extension-account-plans-content { min-height: 0; overflow: auto; } .opportunities-extension-account-plans-tabs { display: flex; gap: 4px; overflow-x: auto; padding: 12px 20px 0; border-bottom: 1px solid #dedbd7; } .opportunities-extension-account-plan-tab { min-height: 38px; flex: 0 0 auto; padding: 7px 12px; border: 0; border-bottom: 3px solid transparent; background: transparent; color: #5f5a55; font-size: 13px; font-weight: 600; cursor: pointer; } .opportunities-extension-account-plan-tab:hover { background: #f0eeeb; color: #312d2a; } .opportunities-extension-account-plan-tab[aria-selected="true"] { border-bottom-color: #00758f; color: #006d7a; } .opportunities-extension-account-plan-tab:focus-visible { outline: 2px solid #00758f; outline-offset: -2px; } .opportunities-extension-account-plan-panel { padding: 24px 20px 28px; } .opportunities-extension-account-plan-panel[hidden] { display: none !important; } .opportunities-extension-account-plan-details { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 0 28px; margin: 0; } .opportunities-extension-account-plan-details > div { display: grid; grid-template-columns: minmax(130px, 38%) minmax(0, 1fr); gap: 12px; padding: 13px 0; border-bottom: 1px solid #ebe8e5; } .opportunities-extension-account-plan-details dt { color: #5f5a55; font-size: 12px; font-weight: 700; } .opportunities-extension-account-plan-details dd { min-width: 0; margin: 0; color: #312d2a; font-size: 13px; overflow-wrap: anywhere; } .opportunities-extension-account-plan-status-indicator { display: inline-block; width: 10px; height: 10px; border-radius: 50%; background: #9b9691; box-shadow: 0 0 0 3px #efedeb; vertical-align: middle; } .opportunities-extension-account-plan-status-indicator[data-active="true"] { background: #3f6f17; box-shadow: 0 0 0 3px #e2f2e5; } .opportunities-extension-account-plan-tab .opportunities-extension-account-plan-status-indicator { margin-right: 8px; } .opportunities-extension-account-plans-skeleton-tabs { display: flex; gap: 16px; min-height: 51px; align-items: center; padding: 0 20px; border-bottom: 1px solid #dedbd7; } .opportunities-extension-account-plans-skeleton-details { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 0 28px; padding: 11px 20px 28px; } .opportunities-extension-account-plans-skeleton-details > div { display: grid; grid-template-columns: minmax(110px, 35%) minmax(0, 1fr); align-items: center; gap: 14px; min-height: 52px; border-bottom: 1px solid #ebe8e5; } .opportunities-extension-account-plans-skeleton-bar { display: block; height: 12px; border-radius: 3px; background: #dedbd7; animation: opportunities-extension-skeleton-pulse 1.15s ease-in-out infinite alternate; } .opportunities-extension-account-plans-skeleton-tabs .opportunities-extension-account-plans-skeleton-bar { height: 14px; } .opportunities-extension-account-plans-skeleton-label { width: 74%; height: 10px; } @keyframes opportunities-extension-skeleton-pulse { from { opacity: .48; } to { opacity: 1; } } @media (prefers-reduced-motion: reduce) { .opportunities-extension-account-plans-skeleton-bar { animation: none; } } .opportunities-extension-account-plans-empty, .opportunities-extension-account-plans-error { margin: 0; padding: 28px 20px; color: #5f5a55; font-size: 14px; } .opportunities-extension-account-plans-error { color: #9f2d20; } .opportunities-extension-preferences-dialog { display: grid; width: min(680px, 100%); max-height: min(720px, calc(100vh - 48px)); grid-template-rows: auto minmax(0, 1fr) auto; overflow: hidden; border: 1px solid #8f8a85; border-radius: 4px; background: #ffffff; box-shadow: 0 8px 24px rgba(0, 0, 0, .24); color: #312d2a; } .opportunities-extension-preferences-header { display: flex; align-items: center; justify-content: space-between; gap: 16px; padding: 18px 20px; border-bottom: 1px solid #dedbd7; } .opportunities-extension-preferences-header h2 { margin: 0; font-size: 20px; font-weight: 700; line-height: 1.2; } .opportunities-extension-preferences-content { min-height: 0; overflow: auto; padding: 20px; } .opportunities-extension-preferences-fieldset { min-width: 0; margin: 0; padding: 0; border: 0; } .opportunities-extension-preferences-fieldset legend { margin-bottom: 12px; padding: 0; color: #312d2a; font-size: 15px; font-weight: 700; line-height: 1.3; } .opportunities-extension-preferences-options { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 4px 20px; } .opportunities-extension-preferences-option { display: flex; align-items: flex-start; gap: 10px; min-width: 0; padding: 9px 8px; border-radius: 4px; color: #312d2a; font-size: 14px; line-height: 1.35; cursor: pointer; } .opportunities-extension-preferences-option:hover { background: #f0eeeb; } .opportunities-extension-preferences-option input { width: 18px; height: 18px; flex: 0 0 18px; margin: 0; accent-color: #00758f; cursor: pointer; } .opportunities-extension-preferences-cache-field { display: grid; width: min(280px, 100%); gap: 6px; margin-top: 24px; color: #312d2a; font-size: 14px; font-weight: 700; } .opportunities-extension-preferences-cache-field select { width: 100%; height: 40px; padding: 0 36px 0 12px; border: 1px solid #8f8a85; border-radius: 4px; background: #ffffff; color: #312d2a; font-size: 14px; font-weight: 400; } .opportunities-extension-preferences-cache-field select:focus { border-color: #00758f; outline: 2px solid #bde7ee; outline-offset: 0; } .opportunities-extension-preferences-footer { display: flex; justify-content: flex-end; padding: 14px 20px; border-top: 1px solid #dedbd7; } .opportunities-extension-button { min-width: 88px; min-height: 36px; padding: 7px 16px; border: 1px solid transparent; border-radius: 4px; font-size: 14px; font-weight: 600; line-height: 1.2; cursor: pointer; } .opportunities-extension-button-primary { border-color: #00758f; background: #00758f; color: #ffffff; } .opportunities-extension-button-primary:hover, .opportunities-extension-button-primary:focus-visible { border-color: #005e73; background: #005e73; outline: none; } .opportunities-extension-debug-panel { position: fixed; right: 24px; bottom: 24px; z-index: 1; width: min(640px, calc(100vw - 48px)); max-height: min(560px, calc(100vh - 120px)); overflow: auto; border: 1px solid #8f8a85; border-radius: 4px; background: #ffffff; box-shadow: 0 2px 8px rgba(0, 0, 0, .16); padding: 16px; color: #000000; } .opportunities-extension-debug-panel[hidden] { display: none !important; } .opportunities-extension-debug-panel h2 { margin: 0; font-size: 18px; font-weight: 700; line-height: 1.25; } .opportunities-extension-debug-panel p { margin: 4px 0 0; color: #5f5a55; font-size: 13px; } .opportunities-extension-debug-panel-header { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; margin-bottom: 12px; } .opportunities-extension-debug-visibility-button { width: 34px; height: 34px; flex: 0 0 34px; color: #312d2a; } .opportunities-extension-debug-visibility-button svg { width: 20px; height: 20px; } .opportunities-extension-debug-list { display: grid; gap: 10px; } .opportunities-extension-debug-item { border: 1px solid #dedbd7; border-left-width: 4px; border-radius: 4px; background: #faf9f8; } .opportunities-extension-debug-item[open] { background: #ffffff; } .opportunities-extension-debug-item-success { border-left-color: #3f6f17; } .opportunities-extension-debug-item-success .opportunities-extension-debug-status { background: #e2f2e5; color: #356d19; } .opportunities-extension-debug-item-failed { border-left-color: #c5331f; } .opportunities-extension-debug-item-failed .opportunities-extension-debug-status { background: #f9e3e1; color: #a52b1c; } .opportunities-extension-debug-item-pending { border-left-color: #6f5a7f; } .opportunities-extension-debug-item-pending .opportunities-extension-debug-status { background: #eee8f2; color: #6f5a7f; } .opportunities-extension-debug-heading { display: flex; align-items: center; justify-content: space-between; gap: 12px; min-height: 42px; padding: 9px 12px 9px 10px; cursor: pointer; list-style: none; } .opportunities-extension-debug-heading::-webkit-details-marker { display: none; } .opportunities-extension-debug-heading::before { content: "⌄"; width: 14px; color: #5f5a55; font-size: 16px; line-height: 1; transform: rotate(-90deg); transition: transform .15s ease; } .opportunities-extension-debug-item[open] .opportunities-extension-debug-heading::before { transform: rotate(0deg); } .opportunities-extension-debug-summary-meta { display: inline-flex; align-items: center; gap: 8px; margin-left: auto; } .opportunities-extension-debug-http-status, .opportunities-extension-debug-status { display: inline-flex; align-items: center; min-height: 22px; padding: 0 8px; border-radius: 999px; font-size: 11px; font-weight: 700; line-height: 1; text-transform: uppercase; } .opportunities-extension-debug-http-status { background: #e8f4f6; color: #006d7a; } .opportunities-extension-debug-status { background: #ebe8e5; color: #5f5a55; } .opportunities-extension-debug-heading strong { font-size: 13px; } .opportunities-extension-debug-item pre { margin: 0; padding: 8px 12px 12px; color: #312d2a; font-family: Consolas, "Courier New", monospace; font-size: 12px; line-height: 1.45; white-space: pre-wrap; word-break: break-word; } .opportunities-extension-debug-empty { padding: 14px; border: 1px dashed #b8b2ad; border-radius: 4px; color: #5f5a55; font-size: 13px; } .opportunities-extension-modal[data-theme="dark"] { background: #1f1f1f; color: #f6f4f2; } .opportunities-extension-modal[data-theme="dark"] .opportunities-extension-shell, .opportunities-extension-modal[data-theme="dark"] .opportunities-extension-body { background: #1f1f1f; } .opportunities-extension-modal[data-theme="dark"] .opportunities-extension-header { border-bottom-color: #4e4a46; background: linear-gradient(90deg, #a8795d 0 11%, #dd5a47 11% 23%, #876e9b 23% 36%, #00a6be 36% 51%, #dfbd76 51% 63%, transparent 63% 100%) top left / 100% 6px no-repeat, #292827; } .opportunities-extension-modal[data-theme="dark"] .opportunities-extension-header h1, .opportunities-extension-modal[data-theme="dark"] .opportunities-extension-stage-dashboard-value, .opportunities-extension-modal[data-theme="dark"] .opportunities-extension-table, .opportunities-extension-modal[data-theme="dark"] .opportunities-extension-field, .opportunities-extension-modal[data-theme="dark"] .opportunities-extension-stage-filter-label, .opportunities-extension-modal[data-theme="dark"] .opportunities-extension-status-filter-label, .opportunities-extension-modal[data-theme="dark"] .opportunities-extension-customer-filter-label, .opportunities-extension-modal[data-theme="dark"] .opportunities-extension-table-search-field { color: #f6f4f2; } .opportunities-extension-modal[data-theme="dark"] .opportunities-extension-header p, .opportunities-extension-modal[data-theme="dark"] .opportunities-extension-account-plans-header p, .opportunities-extension-modal[data-theme="dark"] .opportunities-extension-stage-dashboard-card span, .opportunities-extension-modal[data-theme="dark"] .opportunities-extension-customer-filter-empty, .opportunities-extension-modal[data-theme="dark"] .opportunities-extension-products-detail-heading span, .opportunities-extension-modal[data-theme="dark"] .opportunities-extension-products-empty, .opportunities-extension-modal[data-theme="dark"] .opportunities-extension-account-plans-empty, .opportunities-extension-modal[data-theme="dark"] .opportunities-extension-account-plan-details dt, .opportunities-extension-modal[data-theme="dark"] .opportunities-extension-debug-panel p, .opportunities-extension-modal[data-theme="dark"] .opportunities-extension-debug-empty { color: #c9c5c1; } .opportunities-extension-modal[data-theme="dark"] .opportunities-extension-account-plans-error { color: #ffb4a8; } .opportunities-extension-modal[data-theme="dark"] .opportunities-extension-account-plans-skeleton-bar { background: #5b5753; } .opportunities-extension-modal[data-theme="dark"] .opportunities-extension-account-plan-status-indicator { background: #8f8a85; box-shadow: 0 0 0 3px #3b3937; } .opportunities-extension-modal[data-theme="dark"] .opportunities-extension-account-plan-status-indicator[data-active="true"] { background: #7fb05b; box-shadow: 0 0 0 3px #314527; } .opportunities-extension-modal[data-theme="dark"] .opportunities-extension-form, .opportunities-extension-modal[data-theme="dark"] .opportunities-extension-stage-dashboard-card, .opportunities-extension-modal[data-theme="dark"] .opportunities-extension-table-surface, .opportunities-extension-modal[data-theme="dark"] .opportunities-extension-customer-filter-panel, .opportunities-extension-modal[data-theme="dark"] .opportunities-extension-preferences-dialog, .opportunities-extension-modal[data-theme="dark"] .opportunities-extension-account-plans-dialog, .opportunities-extension-modal[data-theme="dark"] .opportunities-extension-products-table, .opportunities-extension-modal[data-theme="dark"] .opportunities-extension-debug-panel, .opportunities-extension-modal[data-theme="dark"] .opportunities-extension-debug-item[open] { border-color: #4e4a46; background: #292827; } .opportunities-extension-modal[data-theme="dark"] .opportunities-extension-customer-filter-trigger, .opportunities-extension-modal[data-theme="dark"] .opportunities-extension-customer-filter-search, .opportunities-extension-modal[data-theme="dark"] .opportunities-extension-table-search-field input, .opportunities-extension-modal[data-theme="dark"] .opportunities-extension-field select, .opportunities-extension-modal[data-theme="dark"] .opportunities-extension-preferences-cache-field select, .opportunities-extension-modal[data-theme="dark"] .opportunities-extension-stage-filter-button, .opportunities-extension-modal[data-theme="dark"] .opportunities-extension-status-filter-button { border-color: #6a6560; background: #333130; color: #f6f4f2; } .opportunities-extension-modal[data-theme="dark"] .opportunities-extension-customer-filter-trigger::after { border-top-color: #f6f4f2; } .opportunities-extension-modal[data-theme="dark"] .opportunities-extension-select-wrap::after { border-top-color: #f6f4f2; } .opportunities-extension-modal[data-theme="dark"] .opportunities-extension-customer-filter-trigger:disabled { background: #292827; color: #918b86; } .opportunities-extension-modal[data-theme="dark"] .opportunities-extension-customer-filter-search::placeholder, .opportunities-extension-modal[data-theme="dark"] .opportunities-extension-table-search-field input::placeholder { color: #b5b0ab; } .opportunities-extension-modal[data-theme="dark"] .opportunities-extension-customer-filter-select-all, .opportunities-extension-modal[data-theme="dark"] .opportunities-extension-customer-filter-option, .opportunities-extension-modal[data-theme="dark"] .opportunities-extension-preferences-header h2, .opportunities-extension-modal[data-theme="dark"] .opportunities-extension-account-plans-header h2, .opportunities-extension-modal[data-theme="dark"] .opportunities-extension-preferences-fieldset legend, .opportunities-extension-modal[data-theme="dark"] .opportunities-extension-preferences-option, .opportunities-extension-modal[data-theme="dark"] .opportunities-extension-preferences-cache-field, .opportunities-extension-modal[data-theme="dark"] .opportunities-extension-products-detail-heading, .opportunities-extension-modal[data-theme="dark"] .opportunities-extension-account-plan-details dd, .opportunities-extension-modal[data-theme="dark"] .opportunities-extension-debug-heading, .opportunities-extension-modal[data-theme="dark"] .opportunities-extension-debug-item pre, .opportunities-extension-modal[data-theme="dark"] .opportunities-extension-debug-panel h2 { color: #f6f4f2; } .opportunities-extension-modal[data-theme="dark"] .opportunities-extension-customer-filter-select-all, .opportunities-extension-modal[data-theme="dark"] .opportunities-extension-table th { border-bottom-color: #4e4a46; } .opportunities-extension-modal[data-theme="dark"] .opportunities-extension-preferences-header, .opportunities-extension-modal[data-theme="dark"] .opportunities-extension-preferences-footer, .opportunities-extension-modal[data-theme="dark"] .opportunities-extension-account-plans-header, .opportunities-extension-modal[data-theme="dark"] .opportunities-extension-account-plans-tabs, .opportunities-extension-modal[data-theme="dark"] .opportunities-extension-account-plan-details > div, .opportunities-extension-modal[data-theme="dark"] .opportunities-extension-account-plans-skeleton-tabs, .opportunities-extension-modal[data-theme="dark"] .opportunities-extension-account-plans-skeleton-details > div { border-color: #4e4a46; } .opportunities-extension-modal[data-theme="dark"] .opportunities-extension-account-plan-tab { color: #c9c5c1; } .opportunities-extension-modal[data-theme="dark"] .opportunities-extension-account-plan-tab:hover { background: #3b3937; color: #f6f4f2; } .opportunities-extension-modal[data-theme="dark"] .opportunities-extension-account-plan-tab[aria-selected="true"] { border-bottom-color: #43c4d5; color: #91e4ed; } .opportunities-extension-modal[data-theme="dark"] .opportunities-extension-preferences-option:hover { background: #3b3937; } .opportunities-extension-modal[data-theme="dark"] .opportunities-extension-preferences-option input { accent-color: #43c4d5; } .opportunities-extension-modal[data-theme="dark"] .opportunities-extension-button-primary { border-color: #00a6be; background: #008aa6; } .opportunities-extension-modal[data-theme="dark"] .opportunities-extension-button-primary:hover, .opportunities-extension-modal[data-theme="dark"] .opportunities-extension-button-primary:focus-visible { border-color: #43c4d5; background: #006d7a; } .opportunities-extension-modal[data-theme="dark"] .opportunities-extension-customer-filter-option:hover, .opportunities-extension-modal[data-theme="dark"] .opportunities-extension-table tbody tr:hover, .opportunities-extension-modal[data-theme="dark"] .opportunities-extension-sort-button:hover, .opportunities-extension-modal[data-theme="dark"] .opportunities-extension-icon-button:hover, .opportunities-extension-modal[data-theme="dark"] .opportunities-extension-icon-button:focus { background: #3b3937; } .opportunities-extension-modal[data-theme="dark"] .opportunities-extension-table thead, .opportunities-extension-modal[data-theme="dark"] .opportunities-extension-table th, .opportunities-extension-modal[data-theme="dark"] .opportunities-extension-products-table th { background: #333130; color: #f6f4f2; } .opportunities-extension-modal[data-theme="dark"] .opportunities-extension-table td { border-bottom-color: #3f3c39; } .opportunities-extension-modal[data-theme="dark"] .opportunities-extension-products-detail { border-bottom-color: #4e4a46; background: #252423; } .opportunities-extension-modal[data-theme="dark"] .opportunities-extension-products-table th, .opportunities-extension-modal[data-theme="dark"] .opportunities-extension-products-table td { border-bottom-color: #3f3c39; } .opportunities-extension-modal[data-theme="dark"] .opportunities-extension-products-count { border-color: #2f9bae; background: #1d4e55; color: #91e4ed; } .opportunities-extension-modal[data-theme="dark"] .opportunities-extension-products-count:hover, .opportunities-extension-modal[data-theme="dark"] .opportunities-extension-products-count:focus-visible, .opportunities-extension-modal[data-theme="dark"] .opportunities-extension-products-count[aria-expanded="true"] { border-color: #43c4d5; background: #008aa6; color: #ffffff; } .opportunities-extension-modal[data-theme="dark"] .opportunities-extension-products-progress[data-state="running"] { background: #1d4e55; color: #91e4ed; } .opportunities-extension-modal[data-theme="dark"] .opportunities-extension-products-progress[data-state="complete"] { background: #244f2e; color: #a8dfb7; } .opportunities-extension-modal[data-theme="dark"] .opportunities-extension-products-progress[data-state="complete-with-errors"], .opportunities-extension-modal[data-theme="dark"] .opportunities-extension-products-progress[data-state="error"], .opportunities-extension-modal[data-theme="dark"] .opportunities-extension-products-count-error { border-color: #b13b34; background: #5f2926; color: #ffb4ad; } .opportunities-extension-modal[data-theme="dark"] .opportunities-extension-products-cache-timestamp { background: #3b3937; color: #c9c5c1; } .opportunities-extension-modal[data-theme="dark"] .opportunities-extension-table a, .opportunities-extension-modal[data-theme="dark"] .opportunities-extension-copy-button, .opportunities-extension-modal[data-theme="dark"] .opportunities-extension-action-button { color: #43c4d5; } .opportunities-extension-modal[data-theme="dark"] .opportunities-extension-table a:hover, .opportunities-extension-modal[data-theme="dark"] .opportunities-extension-copy-button:hover, .opportunities-extension-modal[data-theme="dark"] .opportunities-extension-copy-button:focus-visible, .opportunities-extension-modal[data-theme="dark"] .opportunities-extension-action-button:hover, .opportunities-extension-modal[data-theme="dark"] .opportunities-extension-action-button:focus-visible { background: #1d4e55; color: #91e4ed; } .opportunities-extension-modal[data-theme="dark"] .opportunities-extension-stage-filter-button[data-stage="SQL"] { color: #43c4d5; } .opportunities-extension-modal[data-theme="dark"] .opportunities-extension-stage-filter-button[data-stage="PIPELINE"] { color: #d1cbc6; } .opportunities-extension-modal[data-theme="dark"] .opportunities-extension-stage-filter-button[data-stage="UPSIDE"] { color: #ffbf62; } .opportunities-extension-modal[data-theme="dark"] .opportunities-extension-stage-filter-button[data-stage="FORECAST"] { color: #d2b7e9; } .opportunities-extension-modal[data-theme="dark"] .opportunities-extension-stage-filter-button[data-stage="WON"] { color: #9bd46a; } .opportunities-extension-modal[data-theme="dark"] .opportunities-extension-stage-dashboard-card[data-stage="SQL"] { border-top-color: #43c4d5; } .opportunities-extension-modal[data-theme="dark"] .opportunities-extension-total-dashboard-card { border-top-color: #f6f4f2; } .opportunities-extension-modal[data-theme="dark"] .opportunities-extension-stage-dashboard-card[data-stage="PIPELINE"] { border-top-color: #c0b9b3; } .opportunities-extension-modal[data-theme="dark"] .opportunities-extension-stage-dashboard-card[data-stage="UPSIDE"] { border-top-color: #ffb84d; } .opportunities-extension-modal[data-theme="dark"] .opportunities-extension-stage-dashboard-card[data-stage="FORECAST"] { border-top-color: #cba7e7; } .opportunities-extension-modal[data-theme="dark"] .opportunities-extension-stage-dashboard-card[data-stage="WON"] { border-top-color: #92cf5b; } .opportunities-extension-modal[data-theme="dark"] .opportunities-extension-stage-filter-button[aria-pressed="true"][data-stage="SQL"] { border-color: #008aa6; background: #008aa6; color: #ffffff; } .opportunities-extension-modal[data-theme="dark"] .opportunities-extension-stage-filter-button[aria-pressed="true"][data-stage="PIPELINE"] { border-color: #6f6863; background: #6f6863; color: #ffffff; } .opportunities-extension-modal[data-theme="dark"] .opportunities-extension-stage-filter-button[aria-pressed="true"][data-stage="UPSIDE"] { border-color: #b96e00; background: #b96e00; color: #ffffff; } .opportunities-extension-modal[data-theme="dark"] .opportunities-extension-stage-filter-button[aria-pressed="true"][data-stage="FORECAST"] { border-color: #755b8b; background: #755b8b; color: #ffffff; } .opportunities-extension-modal[data-theme="dark"] .opportunities-extension-stage-filter-button[aria-pressed="true"][data-stage="WON"] { border-color: #4b7f22; background: #4b7f22; color: #ffffff; } .opportunities-extension-modal[data-theme="dark"] .opportunities-extension-status-filter-button[data-status="OPEN"] { color: #69c985; } .opportunities-extension-modal[data-theme="dark"] .opportunities-extension-status-filter-button[data-status="CLOSED"] { color: #c3cad3; } .opportunities-extension-modal[data-theme="dark"] .opportunities-extension-status-filter-button[data-status="LOST"] { color: #ff9d94; } .opportunities-extension-modal[data-theme="dark"] .opportunities-extension-status-filter-button[data-status="WON"] { color: #83bfff; } .opportunities-extension-modal[data-theme="dark"] .opportunities-extension-status-filter-button[aria-pressed="true"][data-status="OPEN"] { border-color: #197a3d; background: #197a3d; color: #ffffff; } .opportunities-extension-modal[data-theme="dark"] .opportunities-extension-status-filter-button[aria-pressed="true"][data-status="CLOSED"] { border-color: #4b5563; background: #4b5563; color: #ffffff; } .opportunities-extension-modal[data-theme="dark"] .opportunities-extension-status-filter-button[aria-pressed="true"][data-status="LOST"] { border-color: #b13b34; background: #b13b34; color: #ffffff; } .opportunities-extension-modal[data-theme="dark"] .opportunities-extension-status-filter-button[aria-pressed="true"][data-status="WON"] { border-color: #1f5f99; background: #1f5f99; color: #ffffff; } .opportunities-extension-modal[data-theme="dark"] .opportunities-extension-debug-item { border-color: #4e4a46; background: #252423; } .opportunities-extension-modal[data-theme="dark"] .opportunities-extension-icon-button { color: #f6f4f2; } @media (min-width: 1201px) and (max-width: 1450px) { .opportunities-extension-table th:nth-child(1) { width: 16%; } .opportunities-extension-table th:nth-child(3) { width: 18%; } .opportunities-extension-table th:nth-child(4) { width: 12%; } .opportunities-extension-table th:nth-child(6) { width: 7%; } .opportunities-extension-table th:nth-child(7) { width: 8%; } .opportunities-extension-table th:nth-child(10) { width: 10%; } } @media (max-width: 1200px) { .opportunities-extension-table { min-width: 1180px; } .opportunities-extension-table th:nth-child(1) { width: 15%; } .opportunities-extension-table th:nth-child(3) { width: 17%; } .opportunities-extension-table th:nth-child(4) { width: 13%; } .opportunities-extension-table th:nth-child(6) { width: 6%; } .opportunities-extension-table th:nth-child(7) { width: 9%; } .opportunities-extension-table th:nth-child(10) { width: 11%; } } @media (max-width: 700px) { .opportunities-extension-header { padding: 24px 20px 18px; } .opportunities-extension-header::before { left: 20px; } .opportunities-extension-header h1 { font-size: 22px; } .opportunities-extension-header p { font-size: 14px; } .opportunities-extension-body { padding: 16px 20px 24px; } .opportunities-extension-form { align-items: stretch; flex-wrap: wrap; gap: 14px; min-height: auto; padding: 12px; } .opportunities-extension-field { max-width: none; width: 100%; } .opportunities-extension-customer-filter { width: 100%; } .opportunities-extension-customer-filter-trigger { width: 100%; } .opportunities-extension-customer-filter-panel { width: min(360px, calc(100vw - 64px)); } .opportunities-extension-table-search-field { width: 100%; } .opportunities-extension-stage-dashboard { grid-template-columns: repeat(2, minmax(0, 1fr)); } .opportunities-extension-stage-dashboard-value { font-size: 18px; } .opportunities-extension-preferences-modal { padding: 12px; } .opportunities-extension-account-plans-modal { padding: 12px; } .opportunities-extension-preferences-dialog { max-height: calc(100vh - 24px); } .opportunities-extension-account-plans-dialog { max-height: calc(100vh - 24px); } .opportunities-extension-account-plan-details { grid-template-columns: 1fr; } .opportunities-extension-account-plans-skeleton-details { grid-template-columns: 1fr; } .opportunities-extension-preferences-options { grid-template-columns: 1fr; } .opportunities-extension-stage-filter-controls { gap: 6px; } .opportunities-extension-status-filter-controls { gap: 6px; } .opportunities-extension-table-surface { min-width: 0; min-height: calc(100vh - 230px); } .opportunities-extension-debug-panel { right: 12px; bottom: 12px; width: calc(100vw - 24px); max-height: calc(100vh - 96px); } } `; document.head.append(style); } function isAllowedPage() { if (window.location.hostname !== "eeho.fa.us2.oraclecloud.com") { return false; } return ALLOWED_PATHS.some((path) => { return window.location.pathname === path || window.location.pathname.startsWith(`${path}/`); }); } function findInsertionPoint(salesGroup) { const addIconInsideGroup = salesGroup.querySelector(ADD_ICON_SELECTOR); if (addIconInsideGroup) { return addIconInsideGroup; } const container = salesGroup.closest(".flat-grid, .flat-grid-container, [id*='yourapps'], [id*='groupNode']") || salesGroup.parentElement || document.body; const addIconCells = Array.from(container.querySelectorAll(ADD_ICON_SELECTOR)); return addIconCells.find((cell) => { return Boolean(salesGroup.compareDocumentPosition(cell) & Node.DOCUMENT_POSITION_FOLLOWING); }) || null; } function insertTile() { if (document.getElementById(TILE_ID)) { return true; } const salesGroup = document.querySelector(SALES_GROUP_SELECTOR); const addIconCell = salesGroup ? findInsertionPoint(salesGroup) : null; if (!salesGroup || !addIconCell) { return false; } addIconCell.before(createOpportunitiesTile()); return true; } if (insertTile()) { return; } const observer = new MutationObserver(() => { insertTile(); }); observer.observe(document.documentElement, { childList: true, subtree: true }); })();