General source code refactoring

This commit is contained in:
2026-09-17 12:07:01 -03:00
parent 4421b28efe
commit ac817a69d4
92 changed files with 18211 additions and 19394 deletions

View File

@@ -0,0 +1,15 @@
function createOpportunitiesColumns({ formatOracleResponseDate, formatRevenue, formatWinProbability, getNestedValue, getOwnerName }) {
return [
{ 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) },
{ key: "actions", label: "Actions", value: () => "", display: () => "", sortable: false }
];
}

View File

@@ -0,0 +1,146 @@
function createOpportunitiesController({
STAGE_OPTIONS,
STATUS_OPTIONS,
authSessionState,
consumptionPlansState,
consumptionTrendState,
getFiscalPeriodRange,
opportunitiesDashboardState,
opportunitiesFilterState,
opportunitiesQueryState,
opportunitiesTableState,
preferencesState,
productsContentState,
renderProductsProgress,
requestOpportunitiesPage,
requestOpptyProductsInBackground,
requestTokenRelayOnce,
setOpportunitiesTableState
}) {
async function requestOpportunitiesForPeriod(period) {
const dateRange = getFiscalPeriodRange(period, new Date());
const requestVersion = ++opportunitiesQueryState.periodRequestVersion;
if (preferencesState.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 = authSessionState.accessToken || await requestTokenRelayOnce();
if (requestVersion !== opportunitiesQueryState.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)) {
productsContentState.currentProductsRequestId = "";
productsContentState.opptyProductsByNumber = new Map();
productsContentState.expandedProductOptyNumbers = new Set();
productsContentState.productsProgress = {
state: "idle",
completed: 0,
total: 0,
failed: 0,
cacheHits: 0
};
productsContentState.productsLastCachedAt = 0;
consumptionPlansState.accountPlansRequestState = new Map();
consumptionTrendState.consumptionTrendCache = new Map();
consumptionTrendState.consumptionTrendWindowOffsets = new Map();
consumptionTrendState.consumptionTrendSeriesFilters = new Map();
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;
}
}
function resetOpportunitiesTable() {
opportunitiesFilterState.selectedStages = new Set(STAGE_OPTIONS);
opportunitiesFilterState.selectedStatuses = new Set(STATUS_OPTIONS);
opportunitiesFilterState.selectedCustomerKeys = new Set();
opportunitiesFilterState.customerSearch = "";
opportunitiesFilterState.selectedOwnerKeys = new Set();
opportunitiesFilterState.ownerSearch = "";
opportunitiesFilterState.tableSearch = "";
opportunitiesFilterState.hasSavedCustomerFilter = false;
opportunitiesFilterState.hasSavedOwnerFilter = false;
opportunitiesFilterState.customerFilterInitialized = false;
opportunitiesFilterState.ownerFilterInitialized = false;
productsContentState.currentProductsRequestId = "";
productsContentState.opptyProductsByNumber = new Map();
productsContentState.expandedProductOptyNumbers = new Set();
productsContentState.productsProgress = {
state: "idle",
completed: 0,
total: 0,
failed: 0,
cacheHits: 0
};
productsContentState.productsLastCachedAt = 0;
consumptionPlansState.accountPlansRequestState = new Map();
consumptionTrendState.consumptionTrendCache = new Map();
consumptionTrendState.consumptionTrendWindowOffsets = new Map();
consumptionTrendState.consumptionTrendSeriesFilters = new Map();
opportunitiesDashboardState.stageDashboardAmounts = new Map([["TOTAL", 0], ...STAGE_OPTIONS.map((stage) => [stage, 0])]);
opportunitiesTableState.opportunitiesTableState = {
items: [],
status: "idle",
message: "",
sortKey: "",
sortDirection: "ascending"
};
}
function isCurrentPeriodRequest(requestVersion) {
return requestVersion === opportunitiesQueryState.periodRequestVersion;
}
return { requestOpportunitiesForPeriod, requestOpportunities, resetOpportunitiesTable, isCurrentPeriodRequest };
}

View File

@@ -0,0 +1,5 @@
function createOpportunitiesDashboardState({ STAGE_OPTIONS } = {}) {
return {
stageDashboardAmounts: new Map([["TOTAL", 0], ...STAGE_OPTIONS.map((stage) => [stage, 0])])
};
}

View File

@@ -0,0 +1,119 @@
function createOpportunitiesDashboard({
STAGE_DASHBOARD_ID,
STAGE_OPTIONS,
getNestedValue,
opportunitiesDashboardState,
environment
}) {
const { document, requestAnimationFrame, performance } = environment;
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 = opportunitiesDashboardState.stageDashboardAmounts.get(stage) || 0;
opportunitiesDashboardState.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);
}
return { renderStageDashboard, createStageAmountFormatter, animateStageDashboardValue };
}

View File

@@ -0,0 +1,15 @@
function createOpportunitiesFilterState({ STAGE_OPTIONS, STATUS_OPTIONS } = {}) {
return {
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
};
}

View File

@@ -0,0 +1,402 @@
function createOpportunitiesFilters({
CUSTOMER_FILTER_BUTTON_ID,
CUSTOMER_FILTER_LIST_ID,
CUSTOMER_FILTER_PANEL_ID,
CUSTOMER_FILTER_SEARCH_ID,
CUSTOMER_FILTER_SELECT_ALL_ID,
OPPORTUNITIES_COLUMNS,
OWNER_FILTER_BUTTON_ID,
OWNER_FILTER_LIST_ID,
OWNER_FILTER_PANEL_ID,
OWNER_FILTER_SEARCH_ID,
OWNER_FILTER_SELECT_ALL_ID,
STAGE_FILTERS_ID,
STAGE_OPTIONS,
STATUS_FILTERS_ID,
STATUS_OPTIONS,
getCustomerFilterSummary,
getCustomerKey,
getCustomerOptions,
getOwnerKey,
getOwnerOptions,
opportunitiesFilterState,
opportunitiesTableState,
renderOpportunitiesTable,
saveFilterPreferences,
stringifySearchValue,
environment
}) {
const { document } = environment;
function resetCustomerAndOwnerFiltersForPeriod() {
opportunitiesFilterState.selectedCustomerKeys = new Set();
opportunitiesFilterState.selectedOwnerKeys = new Set();
opportunitiesFilterState.customerSearch = "";
opportunitiesFilterState.ownerSearch = "";
opportunitiesFilterState.hasSavedCustomerFilter = false;
opportunitiesFilterState.hasSavedOwnerFilter = false;
opportunitiesFilterState.customerFilterInitialized = false;
opportunitiesFilterState.ownerFilterInitialized = false;
}
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 = opportunitiesFilterState.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 (opportunitiesFilterState.selectedStages.has(stage)) {
opportunitiesFilterState.selectedStages.delete(stage);
} else {
opportunitiesFilterState.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 = opportunitiesFilterState.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 (opportunitiesFilterState.selectedStatuses.has(status)) {
opportunitiesFilterState.selectedStatuses.delete(status);
} else {
opportunitiesFilterState.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 (!opportunitiesFilterState.customerFilterInitialized) {
opportunitiesFilterState.selectedCustomerKeys = new Set(customerOptions.map((customer) => customer.key));
opportunitiesFilterState.customerFilterInitialized = true;
}
}
function resetOwnerFilter(items) {
const ownerOptions = getOwnerOptions(items);
if (!opportunitiesFilterState.ownerFilterInitialized) {
opportunitiesFilterState.selectedOwnerKeys = new Set(ownerOptions.map((owner) => owner.key));
opportunitiesFilterState.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) => opportunitiesFilterState.selectedCustomerKeys.has(customer.key)).length;
trigger.disabled = customerOptions.length === 0;
trigger.textContent = getCustomerFilterSummary(customerOptions.length, selectedCount);
searchInput.value = opportunitiesFilterState.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 = opportunitiesFilterState.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 = opportunitiesFilterState.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 = opportunitiesFilterState.selectedCustomerKeys.has(customer.key);
checkbox.addEventListener("change", () => {
if (checkbox.checked) {
opportunitiesFilterState.selectedCustomerKeys.add(customer.key);
} else {
opportunitiesFilterState.selectedCustomerKeys.delete(customer.key);
}
opportunitiesFilterState.hasSavedCustomerFilter = true;
saveFilterPreferences();
renderCustomerFilter();
renderOpportunitiesTable();
});
const label = document.createElement("span");
label.textContent = customer.label;
option.append(checkbox, label);
list.append(option);
});
}
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) => opportunitiesFilterState.selectedOwnerKeys.has(owner.key)).length;
trigger.disabled = ownerOptions.length === 0;
trigger.textContent = getCustomerFilterSummary(ownerOptions.length, selectedCount)
.replace("customer", "owner");
searchInput.value = opportunitiesFilterState.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 = opportunitiesFilterState.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 = opportunitiesFilterState.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 = opportunitiesFilterState.selectedOwnerKeys.has(owner.key);
checkbox.addEventListener("change", () => {
if (checkbox.checked) {
opportunitiesFilterState.selectedOwnerKeys.add(owner.key);
} else {
opportunitiesFilterState.selectedOwnerKeys.delete(owner.key);
}
opportunitiesFilterState.hasSavedOwnerFilter = true;
saveFilterPreferences();
renderOwnerFilter();
renderOpportunitiesTable();
});
const label = document.createElement("span");
label.textContent = owner.label;
option.append(checkbox, label);
list.append(option);
});
}
function getSortedOpportunities() {
const normalizedSearch = opportunitiesFilterState.tableSearch.trim().toLocaleLowerCase();
const items = opportunitiesTableState.opportunitiesTableState.items.filter((item) => {
return opportunitiesFilterState.selectedStages.has(item.ForecastGroup_c)
&& matchesSelectedStatus(item.StatusCode)
&& opportunitiesFilterState.selectedCustomerKeys.has(getCustomerKey(item))
&& opportunitiesFilterState.selectedOwnerKeys.has(getOwnerKey(item))
&& matchesTableSearch(item, normalizedSearch);
});
const column = OPPORTUNITIES_COLUMNS.find((candidate) => candidate.key === opportunitiesTableState.opportunitiesTableState.sortKey);
if (!column) {
return items;
}
const direction = opportunitiesTableState.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 opportunitiesFilterState.selectedStatuses.has("WON");
}
return opportunitiesFilterState.selectedStatuses.has(statusCode);
}
return { resetCustomerAndOwnerFiltersForPeriod, renderStageFilterButtons, toggleStageFilter, renderStatusFilterButtons, toggleStatusFilter, toggleCustomerFilterPanel, closeCustomerFilterPanel, toggleOwnerFilterPanel, closeOwnerFilterPanel, resetCustomerFilter, resetOwnerFilter, renderCustomerFilter, renderCustomerFilterList, renderOwnerFilter, renderOwnerFilterList, getSortedOpportunities, matchesTableSearch, matchesSelectedStatus };
}

View File

@@ -0,0 +1,55 @@
function createOpportunitiesFiscalPeriods() {
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`;
}
return { getFiscalPeriodRange, createDateRange, formatOracleDate };
}

View File

@@ -0,0 +1,174 @@
function createOpportunitiesFormatters({
opportunitiesTableState
}) {
function getCustomerOptions(items) {
const optionsByKey = new Map();
(items || opportunitiesTableState.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 getOwnerOptions(items) {
const optionsByKey = new Map();
(items || opportunitiesTableState.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 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 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;
}
return { getCustomerOptions, getCustomerKey, getOwnerName, getCustomerFilterSummary, getOwnerOptions, getOwnerKey, stringifySearchValue, displayOpportunityValue, getNestedValue, formatWinProbability, formatRevenue, formatOracleResponseDate };
}

View File

@@ -0,0 +1,118 @@
function createOpportunitiesLinks({
MODAL_ID,
environment
}) {
const { document, navigator, window } = environment;
function createDetailLink(label, href) {
if (label === null || label === undefined || label === "") {
const placeholder = document.createElement("span");
placeholder.textContent = "-";
return placeholder;
}
if (!href) {
const text = document.createElement("span");
text.textContent = String(label);
return text;
}
const link = document.createElement("a");
link.href = href;
link.target = "_blank";
link.rel = "noopener noreferrer";
link.textContent = String(label);
return link;
}
function createCopyOpportunityButton(optyNumber) {
const button = document.createElement("button");
const icon = document.createElementNS("http://www.w3.org/2000/svg", "svg");
const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
button.type = "button";
button.className = "opportunities-extension-copy-button";
button.setAttribute("aria-label", `Copy opportunity number ${optyNumber}`);
button.title = "Copy opportunity number";
button.addEventListener("click", (event) => {
event.preventDefault();
event.stopPropagation();
copyOpportunityNumber(String(optyNumber));
});
icon.setAttribute("viewBox", "0 0 24 24");
icon.setAttribute("aria-hidden", "true");
path.setAttribute("d", "M8 8V5.5A2.5 2.5 0 0 1 10.5 3h8A2.5 2.5 0 0 1 21 5.5v8a2.5 2.5 0 0 1-2.5 2.5H16v2.5a2.5 2.5 0 0 1-2.5 2.5h-8A2.5 2.5 0 0 1 3 18.5v-8A2.5 2.5 0 0 1 5.5 8H8zm2 0h3.5A2.5 2.5 0 0 1 16 10.5v3.5h2.5a.5.5 0 0 0 .5-.5v-8a.5.5 0 0 0-.5-.5h-8a.5.5 0 0 0-.5.5V8zm3.5 2h-8a.5.5 0 0 0-.5.5v8a.5.5 0 0 0 .5.5h8a.5.5 0 0 0 .5-.5v-8a.5.5 0 0 0-.5-.5z");
icon.append(path);
button.append(icon);
return button;
}
async function copyOpportunityNumber(optyNumber) {
try {
if (navigator.clipboard && navigator.clipboard.writeText) {
await navigator.clipboard.writeText(optyNumber);
} else {
const input = document.createElement("textarea");
input.value = optyNumber;
input.setAttribute("readonly", "true");
input.style.position = "fixed";
input.style.opacity = "0";
document.body.append(input);
input.select();
document.execCommand("copy");
input.remove();
}
showCopyConfirmation(`Opty Number ${optyNumber} copied.`);
} catch (error) {
showCopyConfirmation("Unable to copy opportunity number.", true);
}
}
function showCopyConfirmation(message, isError = false) {
const modal = document.getElementById(MODAL_ID);
if (!modal) {
return;
}
let toast = modal.querySelector(".opportunities-extension-copy-toast");
if (!toast) {
toast = document.createElement("div");
toast.className = "opportunities-extension-copy-toast";
toast.setAttribute("role", "status");
modal.append(toast);
}
toast.classList.toggle("opportunities-extension-copy-toast-error", isError);
toast.textContent = message;
toast.hidden = false;
window.clearTimeout(toast.copyTimeout);
toast.copyTimeout = window.setTimeout(() => {
toast.hidden = true;
}, 2400);
}
function createStageBadge(stage) {
const badge = document.createElement("span");
const normalizedStage = typeof stage === "string" ? stage.toUpperCase() : "";
badge.className = "opportunities-extension-stage-badge";
badge.setAttribute("data-stage", normalizedStage);
badge.textContent = normalizedStage || "-";
return badge;
}
function createStatusBadge(status) {
const badge = document.createElement("span");
const normalizedStatus = typeof status === "string" ? status.toUpperCase() : "";
badge.className = "opportunities-extension-status-badge";
badge.setAttribute("data-status", normalizedStatus);
badge.textContent = normalizedStatus || "-";
return badge;
}
return { createDetailLink, createCopyOpportunityButton, copyOpportunityNumber, showCopyConfirmation, createStageBadge, createStatusBadge };
}

View File

@@ -0,0 +1,5 @@
function createOpportunitiesQueryState() {
return {
periodRequestVersion: 0
};
}

View File

@@ -0,0 +1,11 @@
function createOpportunitiesTableState() {
return {
opportunitiesTableState: {
items: [],
status: "idle",
message: "",
sortKey: "",
sortDirection: "ascending"
}
};
}

View File

@@ -0,0 +1,176 @@
function createOpportunitiesTable({
ACCOUNT_DETAIL_URL,
OPPORTUNITIES_COLUMNS,
OPPORTUNITIES_TABLE_ID,
OPPORTUNITY_DETAIL_URL,
createCloudConsumptionButton,
createCopyOpportunityButton,
createDetailLink,
createProductsCountButton,
createProductsDetailRow,
createStageBadge,
createStatusBadge,
displayOpportunityValue,
getNestedValue,
getSortedOpportunities,
opportunitiesTableState,
productsContentState,
renderCustomerFilter,
renderOwnerFilter,
renderStageDashboard,
resetCustomerFilter,
resetOwnerFilter,
environment
}) {
const { document } = environment;
function setOpportunitiesTableState(nextState) {
if (nextState.status === "ready" && Array.isArray(nextState.items)) {
resetCustomerFilter(nextState.items);
resetOwnerFilter(nextState.items);
}
opportunitiesTableState.opportunitiesTableState = {
...opportunitiesTableState.opportunitiesTableState,
...nextState,
sortKey: nextState.items ? "" : opportunitiesTableState.opportunitiesTableState.sortKey,
sortDirection: nextState.items ? "ascending" : opportunitiesTableState.opportunitiesTableState.sortDirection
};
renderOpportunitiesTable();
renderCustomerFilter();
renderOwnerFilter();
}
function renderOpportunitiesTable() {
const table = document.getElementById(OPPORTUNITIES_TABLE_ID);
if (!table) {
return;
}
table.textContent = "";
table.setAttribute("aria-busy", opportunitiesTableState.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.opportunitiesTableState.sortKey === column.key;
header.scope = "col";
if (column.sortable === false) {
const label = document.createElement("span");
label.className = "opportunities-extension-table-header-label";
label.textContent = column.label;
header.append(label);
} else {
header.setAttribute("aria-sort", isSorted ? opportunitiesTableState.opportunitiesTableState.sortDirection : "none");
const sortButton = document.createElement("button");
sortButton.type = "button";
sortButton.className = "opportunities-extension-sort-button";
sortButton.setAttribute("data-sort-direction", isSorted ? opportunitiesTableState.opportunitiesTableState.sortDirection : "none");
sortButton.setAttribute("aria-label", `Sort by ${column.label}${isSorted ? `, ${opportunitiesTableState.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.opportunitiesTableState.status === "loading" || opportunitiesTableState.opportunitiesTableState.status === "error" || opportunitiesTableState.opportunitiesTableState.status === "empty" || (opportunitiesTableState.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.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 === "actions") {
const actionsContent = document.createElement("span");
actionsContent.className = "opportunities-extension-actions-content";
const productsResult = productsContentState.opptyProductsByNumber.get(String(item.OptyNumber));
if (productsResult) {
actionsContent.append(createProductsCountButton(item.OptyNumber, productsResult));
}
actionsContent.append(createCloudConsumptionButton(item));
cell.append(actionsContent);
} 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);
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 && productsContentState.expandedProductOptyNumbers.has(String(item.OptyNumber))) {
tableBody.append(createProductsDetailRow(item));
}
});
}
table.append(caption, tableHead, tableBody);
}
function sortOpportunitiesBy(key) {
const isSameColumn = opportunitiesTableState.opportunitiesTableState.sortKey === key;
opportunitiesTableState.opportunitiesTableState.sortKey = key;
opportunitiesTableState.opportunitiesTableState.sortDirection = isSameColumn && opportunitiesTableState.opportunitiesTableState.sortDirection === "ascending"
? "descending"
: "ascending";
renderOpportunitiesTable();
}
return { setOpportunitiesTableState, renderOpportunitiesTable, sortOpportunitiesBy };
}