General source code refactoring
This commit is contained in:
15
src/features/opportunities/columns.js
Normal file
15
src/features/opportunities/columns.js
Normal 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 }
|
||||
];
|
||||
}
|
||||
146
src/features/opportunities/controller.js
Normal file
146
src/features/opportunities/controller.js
Normal 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 };
|
||||
}
|
||||
5
src/features/opportunities/dashboard-state.js
Normal file
5
src/features/opportunities/dashboard-state.js
Normal file
@@ -0,0 +1,5 @@
|
||||
function createOpportunitiesDashboardState({ STAGE_OPTIONS } = {}) {
|
||||
return {
|
||||
stageDashboardAmounts: new Map([["TOTAL", 0], ...STAGE_OPTIONS.map((stage) => [stage, 0])])
|
||||
};
|
||||
}
|
||||
119
src/features/opportunities/dashboard.js
Normal file
119
src/features/opportunities/dashboard.js
Normal 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 };
|
||||
}
|
||||
15
src/features/opportunities/filter-state.js
Normal file
15
src/features/opportunities/filter-state.js
Normal 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
|
||||
};
|
||||
}
|
||||
402
src/features/opportunities/filters.js
Normal file
402
src/features/opportunities/filters.js
Normal 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 };
|
||||
}
|
||||
55
src/features/opportunities/fiscal-periods.js
Normal file
55
src/features/opportunities/fiscal-periods.js
Normal 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 };
|
||||
}
|
||||
174
src/features/opportunities/formatters.js
Normal file
174
src/features/opportunities/formatters.js
Normal 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 };
|
||||
}
|
||||
118
src/features/opportunities/links.js
Normal file
118
src/features/opportunities/links.js
Normal 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 };
|
||||
}
|
||||
5
src/features/opportunities/query-state.js
Normal file
5
src/features/opportunities/query-state.js
Normal file
@@ -0,0 +1,5 @@
|
||||
function createOpportunitiesQueryState() {
|
||||
return {
|
||||
periodRequestVersion: 0
|
||||
};
|
||||
}
|
||||
11
src/features/opportunities/table-state.js
Normal file
11
src/features/opportunities/table-state.js
Normal file
@@ -0,0 +1,11 @@
|
||||
function createOpportunitiesTableState() {
|
||||
return {
|
||||
opportunitiesTableState: {
|
||||
items: [],
|
||||
status: "idle",
|
||||
message: "",
|
||||
sortKey: "",
|
||||
sortDirection: "ascending"
|
||||
}
|
||||
};
|
||||
}
|
||||
176
src/features/opportunities/table.js
Normal file
176
src/features/opportunities/table.js
Normal 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 };
|
||||
}
|
||||
Reference in New Issue
Block a user