Files
opportunities-extension/dist/firefox/content.js
2026-08-25 10:54:43 -03:00

2346 lines
71 KiB
JavaScript

(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
});
})();