Implemented the customer installed-base view
This commit is contained in:
@@ -11,7 +11,7 @@ function createDebugContentView({
|
||||
const categories = [
|
||||
["all", "All"], ["authentication", "Authentication"],
|
||||
["opportunities", "Opportunities"], ["products", "Products"],
|
||||
["ai", "AI"], ["consumption", "Consumption"], ["other", "Other"]
|
||||
["ai", "AI"], ["consumption", "Consumption"], ["installBase", "Installed Base"], ["other", "Other"]
|
||||
];
|
||||
let selectedCategory = "all";
|
||||
const expandedIds = new Set();
|
||||
@@ -21,6 +21,7 @@ function createDebugContentView({
|
||||
if (entry.name === "requestOpportunities") return "opportunities";
|
||||
if (entry.name === "requestOpptyProducts") return "products";
|
||||
if (entry.name === "requestGenerateAiSummary") return "ai";
|
||||
if (entry.name === "requestInstallBase") return "installBase";
|
||||
if (/^request(?:AccountSummary|AccountPlans|ConsumptionService)/.test(entry.name)) return "consumption";
|
||||
return "other";
|
||||
}
|
||||
|
||||
53
src/features/install-base/action.js
Normal file
53
src/features/install-base/action.js
Normal file
@@ -0,0 +1,53 @@
|
||||
function createInstallBaseAction({ messageTypes, requestConsumerTokenServiceOnce, sendRuntimeMessage,
|
||||
appendBackgroundRequestLog, openInstallBaseModal, environment }) {
|
||||
const { document } = environment;
|
||||
const pending = new Map();
|
||||
|
||||
async function load(partyNumber) {
|
||||
const authorizations = await requestConsumerTokenServiceOnce();
|
||||
const response = await sendRuntimeMessage({
|
||||
type: messageTypes.requestInstallBase, partyNumber,
|
||||
authorizations: { accountSummary: authorizations.accountSummary, installBase: authorizations.installBase }
|
||||
});
|
||||
appendBackgroundRequestLog(response?.debugRequests);
|
||||
if (!response?.ok) throw new Error(response?.error || "Unable to load installed base.");
|
||||
return response.items;
|
||||
}
|
||||
|
||||
function createInstallBaseButton(opportunity) {
|
||||
const button = document.createElement("button");
|
||||
button.type = "button";
|
||||
button.className = "opportunities-extension-action-button";
|
||||
button.title = "View customer installed base";
|
||||
button.setAttribute("aria-label", `View installed base for ${opportunity.CustomerAccount?.PartyUniqueName || "customer"}`);
|
||||
const icon = document.createElementNS("http://www.w3.org/2000/svg", "svg");
|
||||
icon.setAttribute("viewBox", "0 0 24 24");
|
||||
icon.setAttribute("aria-hidden", "true");
|
||||
icon.setAttribute("focusable", "false");
|
||||
const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
|
||||
path.setAttribute("d", "M4 3h16v7H4z M4 14h16v7H4z M7 6.5h.01 M7 17.5h.01 M11 6.5h6 M11 17.5h6 M8 10v4 M16 10v4");
|
||||
path.setAttribute("fill", "none");
|
||||
path.setAttribute("stroke", "currentColor");
|
||||
path.setAttribute("stroke-width", "1.7");
|
||||
path.setAttribute("stroke-linecap", "round");
|
||||
path.setAttribute("stroke-linejoin", "round");
|
||||
icon.append(path);
|
||||
button.append(icon);
|
||||
button.addEventListener("click", async () => {
|
||||
const view = openInstallBaseModal(opportunity, button);
|
||||
const partyNumber = String(opportunity.CustomerAccount?.PartyNumber || "");
|
||||
try {
|
||||
if (!partyNumber) throw new Error("Customer Party Number is unavailable.");
|
||||
if (!pending.has(partyNumber)) {
|
||||
const request = load(partyNumber).finally(() => pending.delete(partyNumber));
|
||||
pending.set(partyNumber, request);
|
||||
}
|
||||
view.render(await pending.get(partyNumber));
|
||||
} catch (error) {
|
||||
view.render([], error.message || "Unable to load installed base.");
|
||||
}
|
||||
});
|
||||
return button;
|
||||
}
|
||||
return { createInstallBaseButton };
|
||||
}
|
||||
43
src/features/install-base/styles.css
Normal file
43
src/features/install-base/styles.css
Normal file
@@ -0,0 +1,43 @@
|
||||
.opportunities-extension-account-plans-dialog.opportunities-extension-install-base-dialog {
|
||||
width: min(1700px, 100%);
|
||||
}
|
||||
.opportunities-extension-install-base-content {
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
overflow: auto;
|
||||
scrollbar-gutter: stable;
|
||||
}
|
||||
.opportunities-extension-install-base-message { margin: 16px; font-size: 13px; }
|
||||
.opportunities-extension-install-base-table {
|
||||
width: 100%; min-width: 1400px; table-layout: fixed; border-collapse: collapse; font-size: 12px;
|
||||
}
|
||||
.opportunities-extension-install-base-table col { width: 7%; }
|
||||
.opportunities-extension-install-base-table .install-base-col-partyName { width: 12%; }
|
||||
.opportunities-extension-install-base-table .install-base-col-productGroupName { width: 13%; }
|
||||
.opportunities-extension-install-base-table .install-base-col-productName { width: 13%; }
|
||||
.opportunities-extension-install-base-table .install-base-col-quantity { width: 5%; }
|
||||
.opportunities-extension-install-base-table .install-base-col-quantityType { width: 8%; }
|
||||
.opportunities-extension-install-base-table th,
|
||||
.opportunities-extension-install-base-table td {
|
||||
padding: 12px 8px; text-align: left; vertical-align: middle; overflow-wrap: anywhere;
|
||||
border-bottom: 1px solid #e8e5e1; line-height: 1.4;
|
||||
}
|
||||
.opportunities-extension-install-base-table th { position: sticky; top: 0; z-index: 1; background: #faf9f8; }
|
||||
.opportunities-extension-install-base-table td[data-format="amount"],
|
||||
.opportunities-extension-install-base-table td[data-format="quantity"] { text-align: right; font-variant-numeric: tabular-nums; }
|
||||
.opportunities-extension-install-base-table td[data-format="date"] { white-space: nowrap; }
|
||||
.opportunities-extension-install-base-status {
|
||||
display: inline-block; padding: 3px 6px; border-radius: 12px; font-size: 10px; font-weight: 600;
|
||||
color: #5f5a55; background: #eceae8; white-space: nowrap;
|
||||
}
|
||||
.opportunities-extension-install-base-status[data-status="ACTIVE"] { color: #17693a; background: #e1f1e5; }
|
||||
.opportunities-extension-install-base-status[data-status="UNSUPPORTED"] { color: #855300; background: #fff0d6; }
|
||||
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-install-base-table th { background: #353331; }
|
||||
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-install-base-table th,
|
||||
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-install-base-table td { border-color: #484541; }
|
||||
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-install-base-status { color: #dedbd7; background: #484541; }
|
||||
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-install-base-status[data-status="ACTIVE"] { color: #b8e4c6; background: #214d34; }
|
||||
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-install-base-status[data-status="UNSUPPORTED"] { color: #ffdfa2; background: #614418; }
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.opportunities-extension-install-base-table .opportunities-extension-account-plans-skeleton-bar { animation: none; }
|
||||
}
|
||||
126
src/features/install-base/view.js
Normal file
126
src/features/install-base/view.js
Normal file
@@ -0,0 +1,126 @@
|
||||
function createInstallBaseView({ MODAL_ID, createCloseIcon, formatOracleResponseDate, environment }) {
|
||||
const { document } = environment;
|
||||
let active = null;
|
||||
const columns = [
|
||||
["partyName", "Customer"], ["productGroupName", "Product Group"], ["productName", "Product"],
|
||||
["currentSupport", "Current Support", "amount"], ["orderNumber", "Order Number"],
|
||||
["supportStatus", "Support Status", "status"], ["quantityType", "Quantity Type"],
|
||||
["quantity", "Quantity", "quantity"], ["supportStartDate", "Support Start Date", "date"],
|
||||
["supportEndDate", "Support End Date", "date"], ["licenseValue", "License Value", "amount"],
|
||||
["shipDate", "Ship Date", "date"]
|
||||
];
|
||||
function element(tag, className, text) {
|
||||
const node = document.createElement(tag);
|
||||
if (className) node.className = className;
|
||||
if (text !== undefined) node.textContent = text;
|
||||
return node;
|
||||
}
|
||||
function closeInstallBaseModal() {
|
||||
if (!active) return false;
|
||||
const previous = active;
|
||||
active = null;
|
||||
previous.modal.remove();
|
||||
if (previous.trigger?.isConnected) previous.trigger.focus();
|
||||
return true;
|
||||
}
|
||||
function createTable(items, loading = false) {
|
||||
const table = element("table", "opportunities-extension-install-base-table");
|
||||
table.setAttribute("aria-label", "Installed base items");
|
||||
const head = element("thead");
|
||||
const header = element("tr");
|
||||
const group = element("colgroup");
|
||||
columns.forEach(([key, label]) => {
|
||||
const col = element("col");
|
||||
col.className = `install-base-col-${key}`;
|
||||
group.append(col);
|
||||
const th = element("th", "", label);
|
||||
th.scope = "col";
|
||||
header.append(th);
|
||||
});
|
||||
head.append(header);
|
||||
const body = element("tbody");
|
||||
(loading ? Array.from({ length: 8 }, () => ({})) : items).forEach(item => {
|
||||
const row = element("tr");
|
||||
columns.forEach(([key, , type]) => {
|
||||
const cell = element("td");
|
||||
if (loading) {
|
||||
const bar = element("span", "opportunities-extension-account-plans-skeleton-bar");
|
||||
bar.setAttribute("aria-hidden", "true");
|
||||
cell.append(bar);
|
||||
} else {
|
||||
const value = item[key];
|
||||
const missing = value === null || value === undefined || value === "";
|
||||
let text = missing ? "-" : String(value).trim();
|
||||
if (!missing && type === "date") text = formatOracleResponseDate(value);
|
||||
if (!missing && (type === "amount" || type === "quantity") && Number.isFinite(Number(value))) {
|
||||
text = new Intl.NumberFormat("en-US", {
|
||||
minimumFractionDigits: type === "amount" ? 2 : 0, maximumFractionDigits: type === "amount" ? 2 : 4
|
||||
}).format(Number(value));
|
||||
}
|
||||
if (type === "status") {
|
||||
const badge = element("span", "opportunities-extension-install-base-status", text);
|
||||
badge.dataset.status = text.toUpperCase();
|
||||
cell.append(badge);
|
||||
} else cell.textContent = text;
|
||||
if (type) cell.dataset.format = type;
|
||||
}
|
||||
row.append(cell);
|
||||
});
|
||||
body.append(row);
|
||||
});
|
||||
table.append(group, head, body);
|
||||
return table;
|
||||
}
|
||||
function openInstallBaseModal(opportunity, trigger) {
|
||||
closeInstallBaseModal();
|
||||
const overlay = document.getElementById(MODAL_ID);
|
||||
if (!overlay || overlay.hidden) return { render() {} };
|
||||
const modal = element("section", "opportunities-extension-account-plans-modal");
|
||||
modal.setAttribute("role", "dialog");
|
||||
modal.setAttribute("aria-modal", "true");
|
||||
modal.setAttribute("aria-busy", "true");
|
||||
modal.setAttribute("aria-labelledby", "opportunities-extension-install-base-title");
|
||||
const dialog = element("div", "opportunities-extension-account-plans-dialog opportunities-extension-install-base-dialog");
|
||||
const header = element("header", "opportunities-extension-account-plans-header");
|
||||
const heading = element("div");
|
||||
const title = element("h2", "", "Installed Base");
|
||||
title.id = "opportunities-extension-install-base-title";
|
||||
heading.append(title, element("p", "", opportunity.CustomerAccount?.PartyUniqueName || "Customer"));
|
||||
const close = element("button", "opportunities-extension-icon-button");
|
||||
close.type = "button";
|
||||
close.title = "Close";
|
||||
close.setAttribute("aria-label", "Close installed base");
|
||||
close.append(createCloseIcon());
|
||||
close.addEventListener("click", closeInstallBaseModal);
|
||||
header.append(heading, close);
|
||||
const content = element("div", "opportunities-extension-install-base-content");
|
||||
content.tabIndex = 0;
|
||||
content.setAttribute("role", "region");
|
||||
content.setAttribute("aria-label", "Installed base results");
|
||||
const status = element("p", "opportunities-extension-install-base-message", "Loading installed base...");
|
||||
status.setAttribute("role", "status");
|
||||
content.append(status, createTable([], true));
|
||||
dialog.append(header, content);
|
||||
modal.append(dialog);
|
||||
overlay.append(modal);
|
||||
active = { modal, trigger };
|
||||
modal.addEventListener("keydown", event => {
|
||||
if (event.key === "Tab") {
|
||||
event.preventDefault();
|
||||
(document.activeElement === close ? content : close).focus();
|
||||
}
|
||||
});
|
||||
close.focus();
|
||||
return { render(items, error = "") {
|
||||
// A closed or replaced dialog must never receive a late response.
|
||||
if (active?.modal !== modal || overlay.hidden) return;
|
||||
modal.setAttribute("aria-busy", "false");
|
||||
content.textContent = "";
|
||||
status.textContent = error || (items.length ? `${items.length} installed base items` : "No installed base items found for this customer.");
|
||||
status.setAttribute("role", error ? "alert" : "status");
|
||||
content.append(status);
|
||||
if (!error && items.length) content.append(createTable(items));
|
||||
} };
|
||||
}
|
||||
return { openInstallBaseModal, closeInstallBaseModal, createTable };
|
||||
}
|
||||
@@ -4,6 +4,7 @@ function createOpportunitiesTable({
|
||||
OPPORTUNITIES_TABLE_ID,
|
||||
OPPORTUNITY_DETAIL_URL,
|
||||
createCloudConsumptionButton,
|
||||
createInstallBaseButton,
|
||||
createCopyOpportunityButton,
|
||||
createDetailLink,
|
||||
createProductsCountButton,
|
||||
@@ -148,7 +149,7 @@ function createOpportunitiesTable({
|
||||
);
|
||||
const customerActions = document.createElement("span");
|
||||
customerActions.className = "opportunities-extension-cell-actions";
|
||||
customerActions.append(createCloudConsumptionButton(item));
|
||||
customerActions.append(createCloudConsumptionButton(item), createInstallBaseButton(item));
|
||||
customerCellContent.append(customerLink, customerActions);
|
||||
cell.append(customerCellContent);
|
||||
} else {
|
||||
|
||||
Reference in New Issue
Block a user