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