Implementation of the opportunity product view

This commit is contained in:
2026-09-07 00:36:56 -03:00
parent 7fef10abd3
commit e4cfb3fa9c
10 changed files with 3082 additions and 18 deletions

View File

@@ -24,14 +24,18 @@
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 = {
@@ -60,6 +64,14 @@
"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;
@@ -73,6 +85,19 @@
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;
@@ -115,6 +140,7 @@
}
ensureExtensionStyles();
ensureProductsBackgroundMessageHandler();
function createOpportunitiesTile() {
const wrapper = document.createElement("div");
@@ -198,6 +224,7 @@
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());
@@ -227,7 +254,19 @@
const subtitleRow = document.createElement("div");
subtitleRow.className = "opportunities-extension-subtitle-row";
subtitleRow.append(subtitle, authBadge);
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);
@@ -698,6 +737,18 @@
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",
@@ -714,6 +765,7 @@
status: "ready",
message: ""
});
requestOpptyProductsInBackground(allItems, token);
}
} catch (error) {
if (isCurrentPeriodRequest(requestVersion)) {
@@ -809,6 +861,180 @@
}
}
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);
@@ -821,6 +1047,17 @@
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: [],
@@ -991,7 +1228,19 @@
item.OptyNumber ? `${OPPORTUNITY_DETAIL_URL}${encodeURIComponent(item.OptyNumber)}` : ""
);
if (column.key === "optyNumber" && 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));
@@ -1014,6 +1263,10 @@
});
tableBody.append(row);
if (item.OptyNumber && expandedProductOptyNumbers.has(String(item.OptyNumber))) {
tableBody.append(createProductsDetailRow(item));
}
});
}
@@ -1663,6 +1916,144 @@
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) {
@@ -2396,6 +2787,24 @@
}
}
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);
@@ -2406,6 +2815,16 @@
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;
@@ -2468,7 +2887,25 @@
});
fieldset.append(options);
content.append(fieldset);
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";
@@ -2479,11 +2916,16 @@
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
opportunityTypeValues: selectedValues,
opptyProductsCacheHours
}));
} catch (error) {
// Continue with the saved selection for the active modal when storage is unavailable.
@@ -2688,6 +3130,66 @@
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;
@@ -3357,6 +3859,128 @@
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;
@@ -3644,6 +4268,34 @@
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;
@@ -3865,6 +4517,8 @@
.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;
@@ -3875,6 +4529,7 @@
.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;
@@ -3885,6 +4540,7 @@
.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;
@@ -3915,6 +4571,8 @@
.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 {
@@ -3959,7 +4617,8 @@
}
.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-table th,
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-products-table th {
background: #333130;
color: #f6f4f2;
}
@@ -3968,6 +4627,53 @@
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;