From 04d0d21760a0c4f9274ff0ea7ff549eff02850eb Mon Sep 17 00:00:00 2001 From: Paulo Porto Date: Tue, 25 Aug 2026 10:54:43 -0300 Subject: [PATCH] Initial commit --- README.md | 42 + dist/chromium/background.js | 216 ++++ dist/chromium/content.js | 2345 +++++++++++++++++++++++++++++++++++ dist/chromium/manifest.json | 28 + dist/firefox/background.js | 216 ++++ dist/firefox/content.js | 2345 +++++++++++++++++++++++++++++++++++ dist/firefox/manifest.json | 35 + package.json | 10 + scripts/build.mjs | 85 ++ src/background.js | 216 ++++ src/content.js | 2345 +++++++++++++++++++++++++++++++++++ 11 files changed, 7883 insertions(+) create mode 100644 README.md create mode 100644 dist/chromium/background.js create mode 100644 dist/chromium/content.js create mode 100644 dist/chromium/manifest.json create mode 100644 dist/firefox/background.js create mode 100644 dist/firefox/content.js create mode 100644 dist/firefox/manifest.json create mode 100644 package.json create mode 100644 scripts/build.mjs create mode 100644 src/background.js create mode 100644 src/content.js diff --git a/README.md b/README.md new file mode 100644 index 0000000..e6ad844 --- /dev/null +++ b/README.md @@ -0,0 +1,42 @@ +# Opportunities Extension + +Extensao de navegador para adicionar o atalho "Opportunities Extension" nas paginas Oracle Fusion permitidas. + +## Estrutura de desenvolvimento + +- `src/content.js`: script unico mantido no projeto. +- `scripts/build.mjs`: gera os pacotes especificos para cada navegador. +- `dist/chromium/`: output para Google Chrome e Microsoft Edge. +- `dist/firefox/`: output para Firefox. + +## Build + +Use o Node.js: + +```bash +npm run build +``` + +O build recria `dist/chromium` e `dist/firefox` a partir da mesma fonte. + +## Como carregar em modo desenvolvimento + +### Chrome ou Microsoft Edge + +1. Abra `chrome://extensions` ou `edge://extensions`. +2. Ative o modo de desenvolvedor. +3. Clique em "Carregar sem compactacao". +4. Selecione a pasta `dist/chromium`. + +### Firefox + +1. Abra `about:debugging#/runtime/this-firefox`. +2. Clique em "Carregar extensao temporaria". +3. Selecione o arquivo `dist/firefox/manifest.json`. + +## Paginas atendidas + +- `https://eeho.fa.us2.oraclecloud.com/hcmUI/faces/FuseWelcome` +- `https://eeho.fa.us2.oraclecloud.com/fscmUI/faces/FuseWelcome` + +Ao encontrar o grupo `#yourapps_groupNode_sales`, a extensao adiciona o tile antes do item `.flat-grid-cell.flat-grid-cell-addicon`. Ao clicar no tile, exibe o alerta `Deu certo`. diff --git a/dist/chromium/background.js b/dist/chromium/background.js new file mode 100644 index 0000000..1e5b984 --- /dev/null +++ b/dist/chromium/background.js @@ -0,0 +1,216 @@ +(function () { + "use strict"; + + const ORACLE_DOMAIN = "eeho.fa.us2.oraclecloud.com"; + const ORACLE_COOKIE_URLS = [ + "https://eeho.fa.us2.oraclecloud.com/", + "https://eeho.fa.us2.oraclecloud.com/hcmUI/faces/FuseWelcome", + "https://eeho.fa.us2.oraclecloud.com/fscmUI/faces/FuseWelcome", + "https://eeho.fa.us2.oraclecloud.com/fscmRestApi/tokenrelay" + ]; + const XSRF_COOKIE_NAME = "XSRF-TOKEN-US2DZ2V_F"; + const XSRF_COOKIE_PREFIX = "XSRF-TOKEN-"; + const runtimeApi = typeof browser !== "undefined" ? browser : chrome; + + runtimeApi.runtime.onMessage.addListener((message, sender, sendResponse) => { + if (!message || message.type !== "opportunitiesExtension.getXsrfToken") { + return false; + } + + getXsrfTokenCookie() + .then((result) => { + sendResponse({ + ok: true, + cookieName: result.cookie ? result.cookie.name : "", + token: result.cookie ? result.cookie.value : "", + matchedCookieNames: result.matchedCookieNames, + lookupDetails: result.lookupDetails + }); + }) + .catch((error) => { + sendResponse({ + ok: false, + cookieName: "", + token: "", + matchedCookieNames: [], + lookupDetails: [], + error: error.message || "Unable to read cookies." + }); + }); + + return true; + }); + + async function getXsrfTokenCookie() { + const lookupDetails = []; + const allCookies = []; + const stores = await getCookieStores(lookupDetails); + + for (const store of stores) { + const exactCookie = await getExactCookieFromUrls(store.id, lookupDetails); + + if (exactCookie) { + allCookies.push(exactCookie); + } + } + + for (const store of stores) { + await collectCookies({ + name: XSRF_COOKIE_NAME, + storeId: store.id + }, allCookies, lookupDetails); + } + + for (const store of stores) { + await collectCookies({ + domain: ORACLE_DOMAIN, + storeId: store.id + }, allCookies, lookupDetails); + + await collectCookies({ + domain: `.${ORACLE_DOMAIN}`, + storeId: store.id + }, allCookies, lookupDetails); + } + + for (const store of stores) { + for (const url of ORACLE_COOKIE_URLS) { + await collectCookies({ + url, + storeId: store.id + }, allCookies, lookupDetails); + } + } + + const uniqueCookies = dedupeCookies(allCookies); + const xsrfCookies = uniqueCookies.filter((cookie) => cookie.name.startsWith(XSRF_COOKIE_PREFIX)); + const exactCookie = xsrfCookies.find((cookie) => cookie.name === XSRF_COOKIE_NAME); + const hostCookie = xsrfCookies.find((cookie) => cookie.domain === ORACLE_DOMAIN || cookie.domain === `.${ORACLE_DOMAIN}`); + + return { + cookie: exactCookie || hostCookie || xsrfCookies[0] || null, + matchedCookieNames: xsrfCookies.map((cookie) => `${cookie.name} (${cookie.domain}${cookie.path})`), + lookupDetails + }; + } + + async function getExactCookieFromUrls(storeId, lookupDetails) { + for (const url of ORACLE_COOKIE_URLS) { + const cookie = await cookiesGet({ + url, + name: XSRF_COOKIE_NAME, + storeId + }); + + lookupDetails.push(`${JSON.stringify({ url, name: XSRF_COOKIE_NAME, storeId })} => ${cookie ? "found" : "not found"}`); + + if (cookie) { + return cookie; + } + } + + return null; + } + + async function getCookieStores(lookupDetails) { + try { + const stores = await cookiesGetAllCookieStores(); + lookupDetails.push(`getAllCookieStores => ${stores.length} store(s)`); + return stores.length ? stores : [{ id: undefined }]; + } catch (error) { + lookupDetails.push(`getAllCookieStores => unavailable (${error.message || "unknown error"})`); + return [{ id: undefined }]; + } + } + + async function collectCookies(details, target, lookupDetails) { + const cleanDetails = removeUndefinedValues(details); + const cookies = await cookiesGetAll(cleanDetails); + target.push(...cookies); + lookupDetails.push(`${JSON.stringify(cleanDetails)} => ${cookies.length} cookie(s)`); + } + + function dedupeCookies(cookies) { + const seen = new Set(); + + return cookies.filter((cookie) => { + const key = `${cookie.name}|${cookie.domain}|${cookie.path}|${cookie.storeId || ""}`; + + if (seen.has(key)) { + return false; + } + + seen.add(key); + return true; + }); + } + + function cookiesGetAll(details) { + if (runtimeApi.cookies.getAll.length <= 1) { + return runtimeApi.cookies.getAll(details); + } + + return new Promise((resolve, reject) => { + runtimeApi.cookies.getAll(details, (cookies) => { + const lastError = runtimeApi.runtime.lastError; + + if (lastError) { + reject(new Error(lastError.message)); + return; + } + + resolve(cookies); + }); + }); + } + + function cookiesGet(details) { + const cleanDetails = removeUndefinedValues(details); + + if (runtimeApi.cookies.get.length <= 1) { + return runtimeApi.cookies.get(cleanDetails); + } + + return new Promise((resolve, reject) => { + runtimeApi.cookies.get(cleanDetails, (cookie) => { + const lastError = runtimeApi.runtime.lastError; + + if (lastError) { + reject(new Error(lastError.message)); + return; + } + + resolve(cookie); + }); + }); + } + + function cookiesGetAllCookieStores() { + if (!runtimeApi.cookies.getAllCookieStores) { + return Promise.resolve([{ id: undefined }]); + } + + if (runtimeApi.cookies.getAllCookieStores.length === 0) { + return runtimeApi.cookies.getAllCookieStores(); + } + + return new Promise((resolve, reject) => { + runtimeApi.cookies.getAllCookieStores((stores) => { + const lastError = runtimeApi.runtime.lastError; + + if (lastError) { + reject(new Error(lastError.message)); + return; + } + + resolve(stores); + }); + }); + } + + function removeUndefinedValues(details) { + return Object.fromEntries( + Object.entries(details).filter((entry) => entry[1] !== undefined) + ); + } +})(); diff --git a/dist/chromium/content.js b/dist/chromium/content.js new file mode 100644 index 0000000..8416ed8 --- /dev/null +++ b/dist/chromium/content.js @@ -0,0 +1,2345 @@ +(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 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 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"]; + let tokenRelayRequest = null; + let accessToken = ""; + let periodRequestVersion = 0; + let authStatus = AUTH_STATUS.idle; + let requestLog = []; + let selectedStages = new Set(STAGE_OPTIONS); + let selectedCustomerKeys = new Set(); + let customerSearch = ""; + 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: "winProbability", label: "Win Probability", value: (item) => getNestedValue(item, ["PrimaryRevenue", "WinProb"]), display: formatWinProbability }, + { key: "customer", label: "Customer", value: (item) => getNestedValue(item, ["CustomerAccount", "PartyUniqueName"]), display: (item) => getNestedValue(item, ["CustomerAccount", "PartyUniqueName"]) }, + { key: "revenue", label: "Revenue", value: (item) => getNestedValue(item, ["PrimaryRevenue", "RevnAmount"]), display: formatRevenue }, + { key: "closeDate", label: "Close Date", value: (item) => item.EffectiveDate, display: (item) => formatOracleResponseDate(item.EffectiveDate) }, + { key: "stage", label: "Stage", value: (item) => item.ForecastGroup_c, display: (item) => item.ForecastGroup_c }, + { key: "status", label: "Status", value: (item) => item.StatusCode, display: (item) => item.StatusCode }, + { 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(); + overlay.id = MODAL_ID; + overlay.className = "opportunities-extension-modal"; + 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); + + header.append(titleBlock, closeButton); + + 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 = "Periodo"; + + 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", () => { + requestOpportunitiesForPeriod(select.value); + }); + + selectWrap.append(select); + field.append(labelText, selectWrap); + + 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; + 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(); + 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); + form.append(field, stageFilter, customerFilter); + body.append(form); + + 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(); + renderCustomerFilter(); + 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() { + const xsrfTokenSource = await getXsrfToken(); + const requestEntry = 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 || "" + } + }); + + if (!xsrfTokenSource.token) { + logRequestFailure(requestEntry, "XSRF token cookie not found."); + throw new Error("XSRF token cookie not found."); + } + + 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); + throw new Error(`Token relay failed with status ${response.status}.`); + } + + let responseData; + + try { + responseData = await response.json(); + } catch (error) { + logRequestSuccess(requestEntry, response, responseBody); + throw error; + } + + if (!responseData || typeof responseData.access_token !== "string" || !responseData.access_token) { + logRequestSuccess(requestEntry, response, responseBody); + 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; + } + } + + async function requestOpportunitiesForPeriod(period) { + const dateRange = getFiscalPeriodRange(period, new Date()); + const requestVersion = ++periodRequestVersion; + + 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); + selectedCustomerKeys = new Set(); + customerSearch = ""; + opportunitiesTableState = { + items: [], + status: "idle", + message: "", + sortKey: "", + sortDirection: "ascending" + }; + } + + function setOpportunitiesTableState(nextState) { + if (nextState.status === "ready" && Array.isArray(nextState.items)) { + resetCustomerFilter(nextState.items); + } + + opportunitiesTableState = { + ...opportunitiesTableState, + ...nextState, + sortKey: nextState.items ? "" : opportunitiesTableState.sortKey, + sortDirection: nextState.items ? "ascending" : opportunitiesTableState.sortDirection + }; + renderOpportunitiesTable(); + renderCustomerFilter(); + } + + 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(); + + 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 === "optyNumber") { + cell.append(createDetailLink( + column.display(item), + item.OptyNumber ? `${OPPORTUNITY_DETAIL_URL}${encodeURIComponent(item.OptyNumber)}` : "" + )); + } 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 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); + } + + renderStageFilterButtons(); + 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() { + 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"); + button.focus(); + return true; + } + + function resetCustomerFilter(items) { + const customerOptions = getCustomerOptions(items); + selectedCustomerKeys = new Set(customerOptions.map((customer) => customer.key)); + customerSearch = ""; + } + + 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); + } + + 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 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 getSortedOpportunities() { + const items = opportunitiesTableState.items.filter((item) => { + return selectedStages.has(item.ForecastGroup_c) && selectedCustomerKeys.has(getCustomerKey(item)); + }); + 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 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 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 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: [ + { op: "$eq", attribute: "RecordSet", value: "ORA_MYSALESTEAMOPTIES" }, + { op: "$eq", attribute: "RecordSet", value: "ORA_MYASSGTERROPTIES" } + ] + }, + { + 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", + "ForecastGroup_c", + "LastUpdateDate", + "PrimaryRevenue.RevnAmountCurcyCode", + "OptyId" + ], + fields: [ + "Name", + "OptyNumber", + "PrimaryRevenue.WinProb", + "CustomerAccount", + "PrimaryRevenue.RevnAmount", + "EffectiveDate", + "StatusCode", + "DealRisk_c", + "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") { + return createDateRange(fiscalStartYear, 5, fiscalStartYear + 1, 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("article"); + item.className = `opportunities-extension-debug-item opportunities-extension-debug-item-${entry.status}`; + + const heading = document.createElement("div"); + heading.className = "opportunities-extension-debug-heading"; + + const name = document.createElement("strong"); + name.textContent = `${entry.id}. ${entry.name}`; + + const status = document.createElement("span"); + status.textContent = entry.status; + + heading.append(name, status); + + 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 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) { + modal.hidden = true; + } + + document.documentElement.classList.remove("opportunities-extension-scroll-lock"); + } + + 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") { + if (closeCustomerFilterPanel()) { + event.preventDefault(); + return; + } + + closeOpportunitiesModal(); + } + }); + } + + 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-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 minmax(0, 1fr); + gap: 16px; + padding: 22px 40px 36px; + background: #f5f4f2; + overflow: hidden; + } + + .opportunities-extension-form { + display: flex; + align-items: end; + 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: 360px; + width: 360px; + gap: 6px; + color: #312d2a; + font-size: 13px; + font-weight: 600; + line-height: 1.3; + } + + .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-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-select-wrap { + position: relative; + display: block; + } + + .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-height: 44px; + 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: auto; + } + + .opportunities-extension-table { + width: 100%; + min-width: 1440px; + 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: 9%; + } + + .opportunities-extension-table th:nth-child(3) { + width: 10%; + } + + .opportunities-extension-table th:nth-child(4) { + width: 21%; + } + + .opportunities-extension-table th:nth-child(5) { + width: 10%; + } + + .opportunities-extension-table th:nth-child(6) { + width: 10%; + } + + .opportunities-extension-table th:nth-child(7) { + width: 9%; + } + + .opportunities-extension-table th:nth-child(8) { + width: 7%; + } + + .opportunities-extension-table th:nth-child(9) { + width: 12%; + } + + .opportunities-extension-table td:nth-child(2), + .opportunities-extension-table td:nth-child(3), + .opportunities-extension-table td:nth-child(5), + .opportunities-extension-table td:nth-child(6), + .opportunities-extension-table td:nth-child(7), + .opportunities-extension-table td:nth-child(8), + .opportunities-extension-table td:nth-child(9) { + 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-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-sort-button { + position: relative; + display: inline-flex; + align-items: center; + width: 100%; + min-height: 44px; + padding: 10px 30px 10px 16px; + border: 0; + background: transparent; + color: inherit; + font: inherit; + font-weight: inherit; + letter-spacing: 0; + text-align: left; + 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-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-success { + border-left-color: #3f6f17; + } + + .opportunities-extension-debug-item-failed { + border-left-color: #c5331f; + } + + .opportunities-extension-debug-item-pending { + border-left-color: #6f5a7f; + } + + .opportunities-extension-debug-heading { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + padding: 10px 12px 0; + } + + .opportunities-extension-debug-heading strong { + font-size: 13px; + } + + .opportunities-extension-debug-heading span { + color: #312d2a; + font-size: 12px; + font-weight: 700; + text-transform: uppercase; + } + + .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; + } + + @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-stage-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 + }); +})(); diff --git a/dist/chromium/manifest.json b/dist/chromium/manifest.json new file mode 100644 index 0000000..ddb6efd --- /dev/null +++ b/dist/chromium/manifest.json @@ -0,0 +1,28 @@ +{ + "manifest_version": 3, + "name": "Opportunities Extension", + "description": "Adiciona um atalho de Opportunities Extension nas paginas Oracle Fusion permitidas.", + "version": "0.1.0", + "permissions": [ + "cookies" + ], + "host_permissions": [ + "https://eeho.fa.us2.oraclecloud.com/*" + ], + "content_scripts": [ + { + "matches": [ + "https://eeho.fa.us2.oraclecloud.com/hcmUI/faces/FuseWelcome*", + "https://eeho.fa.us2.oraclecloud.com/fscmUI/faces/FuseWelcome*" + ], + "js": [ + "content.js" + ], + "all_frames": true, + "run_at": "document_idle" + } + ], + "background": { + "service_worker": "background.js" + } +} diff --git a/dist/firefox/background.js b/dist/firefox/background.js new file mode 100644 index 0000000..1e5b984 --- /dev/null +++ b/dist/firefox/background.js @@ -0,0 +1,216 @@ +(function () { + "use strict"; + + const ORACLE_DOMAIN = "eeho.fa.us2.oraclecloud.com"; + const ORACLE_COOKIE_URLS = [ + "https://eeho.fa.us2.oraclecloud.com/", + "https://eeho.fa.us2.oraclecloud.com/hcmUI/faces/FuseWelcome", + "https://eeho.fa.us2.oraclecloud.com/fscmUI/faces/FuseWelcome", + "https://eeho.fa.us2.oraclecloud.com/fscmRestApi/tokenrelay" + ]; + const XSRF_COOKIE_NAME = "XSRF-TOKEN-US2DZ2V_F"; + const XSRF_COOKIE_PREFIX = "XSRF-TOKEN-"; + const runtimeApi = typeof browser !== "undefined" ? browser : chrome; + + runtimeApi.runtime.onMessage.addListener((message, sender, sendResponse) => { + if (!message || message.type !== "opportunitiesExtension.getXsrfToken") { + return false; + } + + getXsrfTokenCookie() + .then((result) => { + sendResponse({ + ok: true, + cookieName: result.cookie ? result.cookie.name : "", + token: result.cookie ? result.cookie.value : "", + matchedCookieNames: result.matchedCookieNames, + lookupDetails: result.lookupDetails + }); + }) + .catch((error) => { + sendResponse({ + ok: false, + cookieName: "", + token: "", + matchedCookieNames: [], + lookupDetails: [], + error: error.message || "Unable to read cookies." + }); + }); + + return true; + }); + + async function getXsrfTokenCookie() { + const lookupDetails = []; + const allCookies = []; + const stores = await getCookieStores(lookupDetails); + + for (const store of stores) { + const exactCookie = await getExactCookieFromUrls(store.id, lookupDetails); + + if (exactCookie) { + allCookies.push(exactCookie); + } + } + + for (const store of stores) { + await collectCookies({ + name: XSRF_COOKIE_NAME, + storeId: store.id + }, allCookies, lookupDetails); + } + + for (const store of stores) { + await collectCookies({ + domain: ORACLE_DOMAIN, + storeId: store.id + }, allCookies, lookupDetails); + + await collectCookies({ + domain: `.${ORACLE_DOMAIN}`, + storeId: store.id + }, allCookies, lookupDetails); + } + + for (const store of stores) { + for (const url of ORACLE_COOKIE_URLS) { + await collectCookies({ + url, + storeId: store.id + }, allCookies, lookupDetails); + } + } + + const uniqueCookies = dedupeCookies(allCookies); + const xsrfCookies = uniqueCookies.filter((cookie) => cookie.name.startsWith(XSRF_COOKIE_PREFIX)); + const exactCookie = xsrfCookies.find((cookie) => cookie.name === XSRF_COOKIE_NAME); + const hostCookie = xsrfCookies.find((cookie) => cookie.domain === ORACLE_DOMAIN || cookie.domain === `.${ORACLE_DOMAIN}`); + + return { + cookie: exactCookie || hostCookie || xsrfCookies[0] || null, + matchedCookieNames: xsrfCookies.map((cookie) => `${cookie.name} (${cookie.domain}${cookie.path})`), + lookupDetails + }; + } + + async function getExactCookieFromUrls(storeId, lookupDetails) { + for (const url of ORACLE_COOKIE_URLS) { + const cookie = await cookiesGet({ + url, + name: XSRF_COOKIE_NAME, + storeId + }); + + lookupDetails.push(`${JSON.stringify({ url, name: XSRF_COOKIE_NAME, storeId })} => ${cookie ? "found" : "not found"}`); + + if (cookie) { + return cookie; + } + } + + return null; + } + + async function getCookieStores(lookupDetails) { + try { + const stores = await cookiesGetAllCookieStores(); + lookupDetails.push(`getAllCookieStores => ${stores.length} store(s)`); + return stores.length ? stores : [{ id: undefined }]; + } catch (error) { + lookupDetails.push(`getAllCookieStores => unavailable (${error.message || "unknown error"})`); + return [{ id: undefined }]; + } + } + + async function collectCookies(details, target, lookupDetails) { + const cleanDetails = removeUndefinedValues(details); + const cookies = await cookiesGetAll(cleanDetails); + target.push(...cookies); + lookupDetails.push(`${JSON.stringify(cleanDetails)} => ${cookies.length} cookie(s)`); + } + + function dedupeCookies(cookies) { + const seen = new Set(); + + return cookies.filter((cookie) => { + const key = `${cookie.name}|${cookie.domain}|${cookie.path}|${cookie.storeId || ""}`; + + if (seen.has(key)) { + return false; + } + + seen.add(key); + return true; + }); + } + + function cookiesGetAll(details) { + if (runtimeApi.cookies.getAll.length <= 1) { + return runtimeApi.cookies.getAll(details); + } + + return new Promise((resolve, reject) => { + runtimeApi.cookies.getAll(details, (cookies) => { + const lastError = runtimeApi.runtime.lastError; + + if (lastError) { + reject(new Error(lastError.message)); + return; + } + + resolve(cookies); + }); + }); + } + + function cookiesGet(details) { + const cleanDetails = removeUndefinedValues(details); + + if (runtimeApi.cookies.get.length <= 1) { + return runtimeApi.cookies.get(cleanDetails); + } + + return new Promise((resolve, reject) => { + runtimeApi.cookies.get(cleanDetails, (cookie) => { + const lastError = runtimeApi.runtime.lastError; + + if (lastError) { + reject(new Error(lastError.message)); + return; + } + + resolve(cookie); + }); + }); + } + + function cookiesGetAllCookieStores() { + if (!runtimeApi.cookies.getAllCookieStores) { + return Promise.resolve([{ id: undefined }]); + } + + if (runtimeApi.cookies.getAllCookieStores.length === 0) { + return runtimeApi.cookies.getAllCookieStores(); + } + + return new Promise((resolve, reject) => { + runtimeApi.cookies.getAllCookieStores((stores) => { + const lastError = runtimeApi.runtime.lastError; + + if (lastError) { + reject(new Error(lastError.message)); + return; + } + + resolve(stores); + }); + }); + } + + function removeUndefinedValues(details) { + return Object.fromEntries( + Object.entries(details).filter((entry) => entry[1] !== undefined) + ); + } +})(); diff --git a/dist/firefox/content.js b/dist/firefox/content.js new file mode 100644 index 0000000..8416ed8 --- /dev/null +++ b/dist/firefox/content.js @@ -0,0 +1,2345 @@ +(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 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 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"]; + let tokenRelayRequest = null; + let accessToken = ""; + let periodRequestVersion = 0; + let authStatus = AUTH_STATUS.idle; + let requestLog = []; + let selectedStages = new Set(STAGE_OPTIONS); + let selectedCustomerKeys = new Set(); + let customerSearch = ""; + 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: "winProbability", label: "Win Probability", value: (item) => getNestedValue(item, ["PrimaryRevenue", "WinProb"]), display: formatWinProbability }, + { key: "customer", label: "Customer", value: (item) => getNestedValue(item, ["CustomerAccount", "PartyUniqueName"]), display: (item) => getNestedValue(item, ["CustomerAccount", "PartyUniqueName"]) }, + { key: "revenue", label: "Revenue", value: (item) => getNestedValue(item, ["PrimaryRevenue", "RevnAmount"]), display: formatRevenue }, + { key: "closeDate", label: "Close Date", value: (item) => item.EffectiveDate, display: (item) => formatOracleResponseDate(item.EffectiveDate) }, + { key: "stage", label: "Stage", value: (item) => item.ForecastGroup_c, display: (item) => item.ForecastGroup_c }, + { key: "status", label: "Status", value: (item) => item.StatusCode, display: (item) => item.StatusCode }, + { 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(); + overlay.id = MODAL_ID; + overlay.className = "opportunities-extension-modal"; + 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); + + header.append(titleBlock, closeButton); + + 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 = "Periodo"; + + 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", () => { + requestOpportunitiesForPeriod(select.value); + }); + + selectWrap.append(select); + field.append(labelText, selectWrap); + + 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; + 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(); + 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); + form.append(field, stageFilter, customerFilter); + body.append(form); + + 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(); + renderCustomerFilter(); + 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() { + const xsrfTokenSource = await getXsrfToken(); + const requestEntry = 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 || "" + } + }); + + if (!xsrfTokenSource.token) { + logRequestFailure(requestEntry, "XSRF token cookie not found."); + throw new Error("XSRF token cookie not found."); + } + + 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); + throw new Error(`Token relay failed with status ${response.status}.`); + } + + let responseData; + + try { + responseData = await response.json(); + } catch (error) { + logRequestSuccess(requestEntry, response, responseBody); + throw error; + } + + if (!responseData || typeof responseData.access_token !== "string" || !responseData.access_token) { + logRequestSuccess(requestEntry, response, responseBody); + 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; + } + } + + async function requestOpportunitiesForPeriod(period) { + const dateRange = getFiscalPeriodRange(period, new Date()); + const requestVersion = ++periodRequestVersion; + + 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); + selectedCustomerKeys = new Set(); + customerSearch = ""; + opportunitiesTableState = { + items: [], + status: "idle", + message: "", + sortKey: "", + sortDirection: "ascending" + }; + } + + function setOpportunitiesTableState(nextState) { + if (nextState.status === "ready" && Array.isArray(nextState.items)) { + resetCustomerFilter(nextState.items); + } + + opportunitiesTableState = { + ...opportunitiesTableState, + ...nextState, + sortKey: nextState.items ? "" : opportunitiesTableState.sortKey, + sortDirection: nextState.items ? "ascending" : opportunitiesTableState.sortDirection + }; + renderOpportunitiesTable(); + renderCustomerFilter(); + } + + 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(); + + 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 === "optyNumber") { + cell.append(createDetailLink( + column.display(item), + item.OptyNumber ? `${OPPORTUNITY_DETAIL_URL}${encodeURIComponent(item.OptyNumber)}` : "" + )); + } 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 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); + } + + renderStageFilterButtons(); + 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() { + 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"); + button.focus(); + return true; + } + + function resetCustomerFilter(items) { + const customerOptions = getCustomerOptions(items); + selectedCustomerKeys = new Set(customerOptions.map((customer) => customer.key)); + customerSearch = ""; + } + + 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); + } + + 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 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 getSortedOpportunities() { + const items = opportunitiesTableState.items.filter((item) => { + return selectedStages.has(item.ForecastGroup_c) && selectedCustomerKeys.has(getCustomerKey(item)); + }); + 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 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 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 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: [ + { op: "$eq", attribute: "RecordSet", value: "ORA_MYSALESTEAMOPTIES" }, + { op: "$eq", attribute: "RecordSet", value: "ORA_MYASSGTERROPTIES" } + ] + }, + { + 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", + "ForecastGroup_c", + "LastUpdateDate", + "PrimaryRevenue.RevnAmountCurcyCode", + "OptyId" + ], + fields: [ + "Name", + "OptyNumber", + "PrimaryRevenue.WinProb", + "CustomerAccount", + "PrimaryRevenue.RevnAmount", + "EffectiveDate", + "StatusCode", + "DealRisk_c", + "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") { + return createDateRange(fiscalStartYear, 5, fiscalStartYear + 1, 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("article"); + item.className = `opportunities-extension-debug-item opportunities-extension-debug-item-${entry.status}`; + + const heading = document.createElement("div"); + heading.className = "opportunities-extension-debug-heading"; + + const name = document.createElement("strong"); + name.textContent = `${entry.id}. ${entry.name}`; + + const status = document.createElement("span"); + status.textContent = entry.status; + + heading.append(name, status); + + 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 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) { + modal.hidden = true; + } + + document.documentElement.classList.remove("opportunities-extension-scroll-lock"); + } + + 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") { + if (closeCustomerFilterPanel()) { + event.preventDefault(); + return; + } + + closeOpportunitiesModal(); + } + }); + } + + 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-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 minmax(0, 1fr); + gap: 16px; + padding: 22px 40px 36px; + background: #f5f4f2; + overflow: hidden; + } + + .opportunities-extension-form { + display: flex; + align-items: end; + 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: 360px; + width: 360px; + gap: 6px; + color: #312d2a; + font-size: 13px; + font-weight: 600; + line-height: 1.3; + } + + .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-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-select-wrap { + position: relative; + display: block; + } + + .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-height: 44px; + 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: auto; + } + + .opportunities-extension-table { + width: 100%; + min-width: 1440px; + 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: 9%; + } + + .opportunities-extension-table th:nth-child(3) { + width: 10%; + } + + .opportunities-extension-table th:nth-child(4) { + width: 21%; + } + + .opportunities-extension-table th:nth-child(5) { + width: 10%; + } + + .opportunities-extension-table th:nth-child(6) { + width: 10%; + } + + .opportunities-extension-table th:nth-child(7) { + width: 9%; + } + + .opportunities-extension-table th:nth-child(8) { + width: 7%; + } + + .opportunities-extension-table th:nth-child(9) { + width: 12%; + } + + .opportunities-extension-table td:nth-child(2), + .opportunities-extension-table td:nth-child(3), + .opportunities-extension-table td:nth-child(5), + .opportunities-extension-table td:nth-child(6), + .opportunities-extension-table td:nth-child(7), + .opportunities-extension-table td:nth-child(8), + .opportunities-extension-table td:nth-child(9) { + 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-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-sort-button { + position: relative; + display: inline-flex; + align-items: center; + width: 100%; + min-height: 44px; + padding: 10px 30px 10px 16px; + border: 0; + background: transparent; + color: inherit; + font: inherit; + font-weight: inherit; + letter-spacing: 0; + text-align: left; + 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-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-success { + border-left-color: #3f6f17; + } + + .opportunities-extension-debug-item-failed { + border-left-color: #c5331f; + } + + .opportunities-extension-debug-item-pending { + border-left-color: #6f5a7f; + } + + .opportunities-extension-debug-heading { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + padding: 10px 12px 0; + } + + .opportunities-extension-debug-heading strong { + font-size: 13px; + } + + .opportunities-extension-debug-heading span { + color: #312d2a; + font-size: 12px; + font-weight: 700; + text-transform: uppercase; + } + + .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; + } + + @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-stage-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 + }); +})(); diff --git a/dist/firefox/manifest.json b/dist/firefox/manifest.json new file mode 100644 index 0000000..50c12a3 --- /dev/null +++ b/dist/firefox/manifest.json @@ -0,0 +1,35 @@ +{ + "manifest_version": 3, + "name": "Opportunities Extension", + "description": "Adiciona um atalho de Opportunities Extension nas paginas Oracle Fusion permitidas.", + "version": "0.1.0", + "permissions": [ + "cookies" + ], + "host_permissions": [ + "https://eeho.fa.us2.oraclecloud.com/*" + ], + "content_scripts": [ + { + "matches": [ + "https://eeho.fa.us2.oraclecloud.com/hcmUI/faces/FuseWelcome*", + "https://eeho.fa.us2.oraclecloud.com/fscmUI/faces/FuseWelcome*" + ], + "js": [ + "content.js" + ], + "all_frames": true, + "run_at": "document_idle" + } + ], + "background": { + "scripts": [ + "background.js" + ] + }, + "browser_specific_settings": { + "gecko": { + "id": "opportunities-extension@local.dev" + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..956cb9f --- /dev/null +++ b/package.json @@ -0,0 +1,10 @@ +{ + "name": "opportunities-extension", + "version": "0.1.0", + "private": true, + "type": "module", + "scripts": { + "build": "node scripts/build.mjs", + "check": "node --check src/content.js && node scripts/build.mjs --check" + } +} diff --git a/scripts/build.mjs b/scripts/build.mjs new file mode 100644 index 0000000..391101c --- /dev/null +++ b/scripts/build.mjs @@ -0,0 +1,85 @@ +import { mkdir, readFile, rm, writeFile } from "node:fs/promises"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +const rootDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); +const srcDir = path.join(rootDir, "src"); +const distDir = path.join(rootDir, "dist"); +const contentScript = await readFile(path.join(srcDir, "content.js"), "utf8"); +const backgroundScript = await readFile(path.join(srcDir, "background.js"), "utf8"); +const checkOnly = process.argv.includes("--check"); + +const baseManifest = { + manifest_version: 3, + name: "Opportunities Extension", + description: "Adiciona um atalho de Opportunities Extension nas paginas Oracle Fusion permitidas.", + version: "0.1.0", + permissions: [ + "cookies" + ], + host_permissions: [ + "https://eeho.fa.us2.oraclecloud.com/*" + ], + content_scripts: [ + { + matches: [ + "https://eeho.fa.us2.oraclecloud.com/hcmUI/faces/FuseWelcome*", + "https://eeho.fa.us2.oraclecloud.com/fscmUI/faces/FuseWelcome*" + ], + js: ["content.js"], + all_frames: true, + run_at: "document_idle" + } + ] +}; + +const targets = [ + { + name: "chromium", + manifest: { + ...baseManifest, + background: { + service_worker: "background.js" + } + } + }, + { + name: "firefox", + manifest: { + ...baseManifest, + background: { + scripts: ["background.js"] + }, + browser_specific_settings: { + gecko: { + id: "opportunities-extension@local.dev" + } + } + } + } +]; + +if (!checkOnly) { + await rm(distDir, { recursive: true, force: true }); +} + +for (const target of targets) { + const manifestJson = `${JSON.stringify(target.manifest, null, 2)}\n`; + JSON.parse(manifestJson); + + if (checkOnly) { + continue; + } + + const outputDir = path.join(distDir, target.name); + await mkdir(outputDir, { recursive: true }); + await writeFile(path.join(outputDir, "manifest.json"), manifestJson); + await writeFile(path.join(outputDir, "content.js"), contentScript); + await writeFile(path.join(outputDir, "background.js"), backgroundScript); +} + +if (!checkOnly) { + console.log("Build gerado em dist/chromium e dist/firefox."); +} else { + console.log("Build check ok."); +} diff --git a/src/background.js b/src/background.js new file mode 100644 index 0000000..1e5b984 --- /dev/null +++ b/src/background.js @@ -0,0 +1,216 @@ +(function () { + "use strict"; + + const ORACLE_DOMAIN = "eeho.fa.us2.oraclecloud.com"; + const ORACLE_COOKIE_URLS = [ + "https://eeho.fa.us2.oraclecloud.com/", + "https://eeho.fa.us2.oraclecloud.com/hcmUI/faces/FuseWelcome", + "https://eeho.fa.us2.oraclecloud.com/fscmUI/faces/FuseWelcome", + "https://eeho.fa.us2.oraclecloud.com/fscmRestApi/tokenrelay" + ]; + const XSRF_COOKIE_NAME = "XSRF-TOKEN-US2DZ2V_F"; + const XSRF_COOKIE_PREFIX = "XSRF-TOKEN-"; + const runtimeApi = typeof browser !== "undefined" ? browser : chrome; + + runtimeApi.runtime.onMessage.addListener((message, sender, sendResponse) => { + if (!message || message.type !== "opportunitiesExtension.getXsrfToken") { + return false; + } + + getXsrfTokenCookie() + .then((result) => { + sendResponse({ + ok: true, + cookieName: result.cookie ? result.cookie.name : "", + token: result.cookie ? result.cookie.value : "", + matchedCookieNames: result.matchedCookieNames, + lookupDetails: result.lookupDetails + }); + }) + .catch((error) => { + sendResponse({ + ok: false, + cookieName: "", + token: "", + matchedCookieNames: [], + lookupDetails: [], + error: error.message || "Unable to read cookies." + }); + }); + + return true; + }); + + async function getXsrfTokenCookie() { + const lookupDetails = []; + const allCookies = []; + const stores = await getCookieStores(lookupDetails); + + for (const store of stores) { + const exactCookie = await getExactCookieFromUrls(store.id, lookupDetails); + + if (exactCookie) { + allCookies.push(exactCookie); + } + } + + for (const store of stores) { + await collectCookies({ + name: XSRF_COOKIE_NAME, + storeId: store.id + }, allCookies, lookupDetails); + } + + for (const store of stores) { + await collectCookies({ + domain: ORACLE_DOMAIN, + storeId: store.id + }, allCookies, lookupDetails); + + await collectCookies({ + domain: `.${ORACLE_DOMAIN}`, + storeId: store.id + }, allCookies, lookupDetails); + } + + for (const store of stores) { + for (const url of ORACLE_COOKIE_URLS) { + await collectCookies({ + url, + storeId: store.id + }, allCookies, lookupDetails); + } + } + + const uniqueCookies = dedupeCookies(allCookies); + const xsrfCookies = uniqueCookies.filter((cookie) => cookie.name.startsWith(XSRF_COOKIE_PREFIX)); + const exactCookie = xsrfCookies.find((cookie) => cookie.name === XSRF_COOKIE_NAME); + const hostCookie = xsrfCookies.find((cookie) => cookie.domain === ORACLE_DOMAIN || cookie.domain === `.${ORACLE_DOMAIN}`); + + return { + cookie: exactCookie || hostCookie || xsrfCookies[0] || null, + matchedCookieNames: xsrfCookies.map((cookie) => `${cookie.name} (${cookie.domain}${cookie.path})`), + lookupDetails + }; + } + + async function getExactCookieFromUrls(storeId, lookupDetails) { + for (const url of ORACLE_COOKIE_URLS) { + const cookie = await cookiesGet({ + url, + name: XSRF_COOKIE_NAME, + storeId + }); + + lookupDetails.push(`${JSON.stringify({ url, name: XSRF_COOKIE_NAME, storeId })} => ${cookie ? "found" : "not found"}`); + + if (cookie) { + return cookie; + } + } + + return null; + } + + async function getCookieStores(lookupDetails) { + try { + const stores = await cookiesGetAllCookieStores(); + lookupDetails.push(`getAllCookieStores => ${stores.length} store(s)`); + return stores.length ? stores : [{ id: undefined }]; + } catch (error) { + lookupDetails.push(`getAllCookieStores => unavailable (${error.message || "unknown error"})`); + return [{ id: undefined }]; + } + } + + async function collectCookies(details, target, lookupDetails) { + const cleanDetails = removeUndefinedValues(details); + const cookies = await cookiesGetAll(cleanDetails); + target.push(...cookies); + lookupDetails.push(`${JSON.stringify(cleanDetails)} => ${cookies.length} cookie(s)`); + } + + function dedupeCookies(cookies) { + const seen = new Set(); + + return cookies.filter((cookie) => { + const key = `${cookie.name}|${cookie.domain}|${cookie.path}|${cookie.storeId || ""}`; + + if (seen.has(key)) { + return false; + } + + seen.add(key); + return true; + }); + } + + function cookiesGetAll(details) { + if (runtimeApi.cookies.getAll.length <= 1) { + return runtimeApi.cookies.getAll(details); + } + + return new Promise((resolve, reject) => { + runtimeApi.cookies.getAll(details, (cookies) => { + const lastError = runtimeApi.runtime.lastError; + + if (lastError) { + reject(new Error(lastError.message)); + return; + } + + resolve(cookies); + }); + }); + } + + function cookiesGet(details) { + const cleanDetails = removeUndefinedValues(details); + + if (runtimeApi.cookies.get.length <= 1) { + return runtimeApi.cookies.get(cleanDetails); + } + + return new Promise((resolve, reject) => { + runtimeApi.cookies.get(cleanDetails, (cookie) => { + const lastError = runtimeApi.runtime.lastError; + + if (lastError) { + reject(new Error(lastError.message)); + return; + } + + resolve(cookie); + }); + }); + } + + function cookiesGetAllCookieStores() { + if (!runtimeApi.cookies.getAllCookieStores) { + return Promise.resolve([{ id: undefined }]); + } + + if (runtimeApi.cookies.getAllCookieStores.length === 0) { + return runtimeApi.cookies.getAllCookieStores(); + } + + return new Promise((resolve, reject) => { + runtimeApi.cookies.getAllCookieStores((stores) => { + const lastError = runtimeApi.runtime.lastError; + + if (lastError) { + reject(new Error(lastError.message)); + return; + } + + resolve(stores); + }); + }); + } + + function removeUndefinedValues(details) { + return Object.fromEntries( + Object.entries(details).filter((entry) => entry[1] !== undefined) + ); + } +})(); diff --git a/src/content.js b/src/content.js new file mode 100644 index 0000000..8416ed8 --- /dev/null +++ b/src/content.js @@ -0,0 +1,2345 @@ +(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 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 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"]; + let tokenRelayRequest = null; + let accessToken = ""; + let periodRequestVersion = 0; + let authStatus = AUTH_STATUS.idle; + let requestLog = []; + let selectedStages = new Set(STAGE_OPTIONS); + let selectedCustomerKeys = new Set(); + let customerSearch = ""; + 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: "winProbability", label: "Win Probability", value: (item) => getNestedValue(item, ["PrimaryRevenue", "WinProb"]), display: formatWinProbability }, + { key: "customer", label: "Customer", value: (item) => getNestedValue(item, ["CustomerAccount", "PartyUniqueName"]), display: (item) => getNestedValue(item, ["CustomerAccount", "PartyUniqueName"]) }, + { key: "revenue", label: "Revenue", value: (item) => getNestedValue(item, ["PrimaryRevenue", "RevnAmount"]), display: formatRevenue }, + { key: "closeDate", label: "Close Date", value: (item) => item.EffectiveDate, display: (item) => formatOracleResponseDate(item.EffectiveDate) }, + { key: "stage", label: "Stage", value: (item) => item.ForecastGroup_c, display: (item) => item.ForecastGroup_c }, + { key: "status", label: "Status", value: (item) => item.StatusCode, display: (item) => item.StatusCode }, + { 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(); + overlay.id = MODAL_ID; + overlay.className = "opportunities-extension-modal"; + 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); + + header.append(titleBlock, closeButton); + + 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 = "Periodo"; + + 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", () => { + requestOpportunitiesForPeriod(select.value); + }); + + selectWrap.append(select); + field.append(labelText, selectWrap); + + 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; + 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(); + 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); + form.append(field, stageFilter, customerFilter); + body.append(form); + + 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(); + renderCustomerFilter(); + 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() { + const xsrfTokenSource = await getXsrfToken(); + const requestEntry = 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 || "" + } + }); + + if (!xsrfTokenSource.token) { + logRequestFailure(requestEntry, "XSRF token cookie not found."); + throw new Error("XSRF token cookie not found."); + } + + 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); + throw new Error(`Token relay failed with status ${response.status}.`); + } + + let responseData; + + try { + responseData = await response.json(); + } catch (error) { + logRequestSuccess(requestEntry, response, responseBody); + throw error; + } + + if (!responseData || typeof responseData.access_token !== "string" || !responseData.access_token) { + logRequestSuccess(requestEntry, response, responseBody); + 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; + } + } + + async function requestOpportunitiesForPeriod(period) { + const dateRange = getFiscalPeriodRange(period, new Date()); + const requestVersion = ++periodRequestVersion; + + 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); + selectedCustomerKeys = new Set(); + customerSearch = ""; + opportunitiesTableState = { + items: [], + status: "idle", + message: "", + sortKey: "", + sortDirection: "ascending" + }; + } + + function setOpportunitiesTableState(nextState) { + if (nextState.status === "ready" && Array.isArray(nextState.items)) { + resetCustomerFilter(nextState.items); + } + + opportunitiesTableState = { + ...opportunitiesTableState, + ...nextState, + sortKey: nextState.items ? "" : opportunitiesTableState.sortKey, + sortDirection: nextState.items ? "ascending" : opportunitiesTableState.sortDirection + }; + renderOpportunitiesTable(); + renderCustomerFilter(); + } + + 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(); + + 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 === "optyNumber") { + cell.append(createDetailLink( + column.display(item), + item.OptyNumber ? `${OPPORTUNITY_DETAIL_URL}${encodeURIComponent(item.OptyNumber)}` : "" + )); + } 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 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); + } + + renderStageFilterButtons(); + 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() { + 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"); + button.focus(); + return true; + } + + function resetCustomerFilter(items) { + const customerOptions = getCustomerOptions(items); + selectedCustomerKeys = new Set(customerOptions.map((customer) => customer.key)); + customerSearch = ""; + } + + 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); + } + + 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 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 getSortedOpportunities() { + const items = opportunitiesTableState.items.filter((item) => { + return selectedStages.has(item.ForecastGroup_c) && selectedCustomerKeys.has(getCustomerKey(item)); + }); + 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 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 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 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: [ + { op: "$eq", attribute: "RecordSet", value: "ORA_MYSALESTEAMOPTIES" }, + { op: "$eq", attribute: "RecordSet", value: "ORA_MYASSGTERROPTIES" } + ] + }, + { + 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", + "ForecastGroup_c", + "LastUpdateDate", + "PrimaryRevenue.RevnAmountCurcyCode", + "OptyId" + ], + fields: [ + "Name", + "OptyNumber", + "PrimaryRevenue.WinProb", + "CustomerAccount", + "PrimaryRevenue.RevnAmount", + "EffectiveDate", + "StatusCode", + "DealRisk_c", + "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") { + return createDateRange(fiscalStartYear, 5, fiscalStartYear + 1, 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("article"); + item.className = `opportunities-extension-debug-item opportunities-extension-debug-item-${entry.status}`; + + const heading = document.createElement("div"); + heading.className = "opportunities-extension-debug-heading"; + + const name = document.createElement("strong"); + name.textContent = `${entry.id}. ${entry.name}`; + + const status = document.createElement("span"); + status.textContent = entry.status; + + heading.append(name, status); + + 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 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) { + modal.hidden = true; + } + + document.documentElement.classList.remove("opportunities-extension-scroll-lock"); + } + + 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") { + if (closeCustomerFilterPanel()) { + event.preventDefault(); + return; + } + + closeOpportunitiesModal(); + } + }); + } + + 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-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 minmax(0, 1fr); + gap: 16px; + padding: 22px 40px 36px; + background: #f5f4f2; + overflow: hidden; + } + + .opportunities-extension-form { + display: flex; + align-items: end; + 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: 360px; + width: 360px; + gap: 6px; + color: #312d2a; + font-size: 13px; + font-weight: 600; + line-height: 1.3; + } + + .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-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-select-wrap { + position: relative; + display: block; + } + + .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-height: 44px; + 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: auto; + } + + .opportunities-extension-table { + width: 100%; + min-width: 1440px; + 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: 9%; + } + + .opportunities-extension-table th:nth-child(3) { + width: 10%; + } + + .opportunities-extension-table th:nth-child(4) { + width: 21%; + } + + .opportunities-extension-table th:nth-child(5) { + width: 10%; + } + + .opportunities-extension-table th:nth-child(6) { + width: 10%; + } + + .opportunities-extension-table th:nth-child(7) { + width: 9%; + } + + .opportunities-extension-table th:nth-child(8) { + width: 7%; + } + + .opportunities-extension-table th:nth-child(9) { + width: 12%; + } + + .opportunities-extension-table td:nth-child(2), + .opportunities-extension-table td:nth-child(3), + .opportunities-extension-table td:nth-child(5), + .opportunities-extension-table td:nth-child(6), + .opportunities-extension-table td:nth-child(7), + .opportunities-extension-table td:nth-child(8), + .opportunities-extension-table td:nth-child(9) { + 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-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-sort-button { + position: relative; + display: inline-flex; + align-items: center; + width: 100%; + min-height: 44px; + padding: 10px 30px 10px 16px; + border: 0; + background: transparent; + color: inherit; + font: inherit; + font-weight: inherit; + letter-spacing: 0; + text-align: left; + 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-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-success { + border-left-color: #3f6f17; + } + + .opportunities-extension-debug-item-failed { + border-left-color: #c5331f; + } + + .opportunities-extension-debug-item-pending { + border-left-color: #6f5a7f; + } + + .opportunities-extension-debug-heading { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + padding: 10px 12px 0; + } + + .opportunities-extension-debug-heading strong { + font-size: 13px; + } + + .opportunities-extension-debug-heading span { + color: #312d2a; + font-size: 12px; + font-weight: 700; + text-transform: uppercase; + } + + .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; + } + + @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-stage-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 + }); +})();