(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 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 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 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" ]; let tokenRelayRequest = null; let accessToken = ""; let periodRequestVersion = 0; let authStatus = AUTH_STATUS.idle; let requestLog = []; 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 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) } ]; const ALLOWED_PATHS = [ "/hcmUI/faces/FuseWelcome", "/fscmUI/faces/FuseWelcome" ]; if (!isAllowedPage()) { return; } ensureExtensionStyles(); 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(); 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; accessToken = ""; periodRequestVersion = 0; authStatus = AUTH_STATUS.idle; resetOpportunitiesTable(); selectedOpportunityTypeValues = new Set(getSavedOpportunityTypeValues()); 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"; subtitleRow.append(subtitle, authBadge); 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; return token; }) .catch((error) => { accessToken = ""; authStatus = AUTH_STATUS.unauthenticated; throw error; }) .finally(() => { updateAuthBadge(authStatus); }); } else { updateAuthBadge(authStatus); } return tokenRelayRequest; } 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)) { 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: "" }); } } 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 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; 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"; 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 === "name" || column.key === "optyNumber") { const opportunityLink = createDetailLink( column.display(item), item.OptyNumber ? `${OPPORTUNITY_DETAIL_URL}${encodeURIComponent(item.OptyNumber)}` : "" ); 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); }); } 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; } 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(); } 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}: ${value}`) .join("\n"); } 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 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.`; 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", entry.requestBody || "(none)"); appendDebugBlock(details, "Response Headers", formatHeaderBlock(entry.responseHeaders)); appendDebugBlock(details, "Response Body", entry.responseBody || "(none)"); item.append(heading, details); list.append(item); }); panel.append(title, summary, list); } 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(); modal.hidden = true; } 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 openPreferencesModal() { const overlay = document.getElementById(MODAL_ID); if (!overlay) { return; } const existingModal = document.getElementById(PREFERENCES_MODAL_ID); if (existingModal) { 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); content.append(fieldset); 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); selectedOpportunityTypeValues = new Set(selectedValues); try { localStorage.setItem(OPPORTUNITY_TYPE_PREFERENCES_STORAGE_KEY, JSON.stringify({ opportunityTypeValues: selectedValues })); } 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 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-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: 17%; } .opportunities-extension-table th:nth-child(2) { width: 8%; } .opportunities-extension-table th:nth-child(3) { width: 19%; } .opportunities-extension-table th:nth-child(4) { width: 12%; } .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: 8%; } .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 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-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-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-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-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-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 0 4px; font-size: 18px; font-weight: 700; line-height: 1.25; } .opportunities-extension-debug-panel p { margin: 0 0 12px; color: #5f5a55; font-size: 13px; } .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-stage-dashboard-card span, .opportunities-extension-modal[data-theme="dark"] .opportunities-extension-customer-filter-empty, .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-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-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-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-preferences-fieldset legend, .opportunities-extension-modal[data-theme="dark"] .opportunities-extension-preferences-option, .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 { border-color: #4e4a46; } .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 { 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-table a, .opportunities-extension-modal[data-theme="dark"] .opportunities-extension-copy-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 { 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-preferences-dialog { max-height: calc(100vh - 24px); } .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 }); })();