Implemented the customer service requests view

This commit is contained in:
2026-09-22 17:55:38 -03:00
parent 3ee8eb6f21
commit ea164cdcd6
24 changed files with 1158 additions and 20 deletions

View File

@@ -11,7 +11,8 @@ function createDebugContentView({
const categories = [
["all", "All"], ["authentication", "Authentication"],
["opportunities", "Opportunities"], ["products", "Products"],
["ai", "AI"], ["consumption", "Consumption"], ["installBase", "Installed Base"], ["other", "Other"]
["ai", "AI"], ["consumption", "Consumption"], ["installBase", "Installed Base"],
["serviceRequests", "Service Requests"], ["other", "Other"]
];
let selectedCategory = "all";
const expandedIds = new Set();
@@ -22,6 +23,7 @@ function createDebugContentView({
if (entry.name === "requestOpptyProducts") return "products";
if (entry.name === "requestGenerateAiSummary") return "ai";
if (entry.name === "requestInstallBase") return "installBase";
if (entry.name === "requestCustomerServiceRequest") return "serviceRequests";
if (/^request(?:AccountSummary|AccountPlans|ConsumptionService)/.test(entry.name)) return "consumption";
return "other";
}

View File

@@ -72,11 +72,16 @@
}
.opportunities-extension-cell-actions .opportunities-extension-action-button {
flex: 0 0 24px;
width: 24px;
height: 24px;
padding: 2px;
}
.opportunities-extension-table > tbody > tr > td[data-column="customer"] .opportunities-extension-cell-actions {
min-width: 84px;
}
.opportunities-extension-cell-actions .opportunities-extension-action-button svg {
width: 18px;
height: 18px;

View File

@@ -5,6 +5,7 @@ function createOpportunitiesTable({
OPPORTUNITY_DETAIL_URL,
createCloudConsumptionButton,
createInstallBaseButton,
createServiceRequestsButton,
createCopyOpportunityButton,
createDetailLink,
createProductsCountButton,
@@ -149,7 +150,7 @@ function createOpportunitiesTable({
);
const customerActions = document.createElement("span");
customerActions.className = "opportunities-extension-cell-actions";
customerActions.append(createCloudConsumptionButton(item), createInstallBaseButton(item));
customerActions.append(createCloudConsumptionButton(item), createInstallBaseButton(item), createServiceRequestsButton(item));
customerCellContent.append(customerLink, customerActions);
cell.append(customerCellContent);
} else {

View File

@@ -0,0 +1,52 @@
function createServiceRequestsAction({ messageTypes, requestConsumerTokenServiceOnce, sendRuntimeMessage,
appendBackgroundRequestLog, openServiceRequestsModal, environment }) {
const { document } = environment;
const pending = new Map();
async function load(partyNumber) {
const authorizations = await requestConsumerTokenServiceOnce();
const response = await sendRuntimeMessage({
type: messageTypes.requestCustomerServiceRequest, partyNumber,
authorizations: { accountSummary: authorizations.accountSummary, customerServiceRequests: authorizations.customerServiceRequests }
});
appendBackgroundRequestLog(response?.debugRequests);
if (!response?.ok) throw new Error(response?.error || "Unable to load service requests.");
return response.items;
}
function createServiceRequestsButton(opportunity) {
const button = document.createElement("button");
button.type = "button";
button.className = "opportunities-extension-action-button";
button.title = "View customer service requests";
button.setAttribute("aria-label", `View service requests 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 14v-3a8 8 0 0 1 16 0v3 M4 11H3v7h4v-7H4 M20 11h1v7h-4v-7h3 M20 18v1a2 2 0 0 1-2 2h-6");
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 = openServiceRequestsModal(opportunity, button);
const partyNumber = String(opportunity.CustomerAccount?.PartyNumber || "");
try {
if (!partyNumber) throw new Error("Customer Party Number is unavailable.");
if (!pending.has(partyNumber)) {
pending.set(partyNumber, load(partyNumber).finally(() => pending.delete(partyNumber)));
}
view.render(await pending.get(partyNumber));
} catch (error) {
view.render([], error.message || "Unable to load service requests.");
}
});
return button;
}
return { createServiceRequestsButton };
}

View File

@@ -0,0 +1,34 @@
.opportunities-extension-account-plans-dialog.opportunities-extension-service-requests-dialog { width: min(1700px, 100%); }
.opportunities-extension-service-requests-content { min-width: 0; min-height: 0; overflow: auto; scrollbar-gutter: stable; }
.opportunities-extension-service-requests-message { margin: 16px; font-size: 13px; }
.opportunities-extension-service-requests-table {
width: 100%; min-width: 1200px; table-layout: fixed; border-collapse: collapse; font-size: 12px;
}
.opportunities-extension-service-requests-table col { width: 7%; }
.opportunities-extension-service-requests-table .service-requests-col-name { width: 17%; }
.opportunities-extension-service-requests-table .service-requests-col-id { width: 10%; }
.opportunities-extension-service-requests-table .service-requests-col-severity { width: 5%; }
.opportunities-extension-service-requests-table .service-requests-col-subStatus { width: 14%; }
.opportunities-extension-service-requests-table .service-requests-col-productName { width: 12%; }
.opportunities-extension-service-requests-table th,
.opportunities-extension-service-requests-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-service-requests-table th { position: sticky; top: 0; z-index: 1; background: #faf9f8; }
.opportunities-extension-service-requests-table td[data-field="created"],
.opportunities-extension-service-requests-table td[data-field="lastUpdated"],
.opportunities-extension-service-requests-table td[data-field="id"] { white-space: nowrap; font-variant-numeric: tabular-nums; }
.opportunities-extension-service-request-status {
display: inline-block; padding: 3px 6px; border-radius: 12px; font-size: 10px; font-weight: 600;
color: #5f5a55; background: #eceae8;
}
.opportunities-extension-service-request-status[data-status="OPEN"] { color: #17693a; background: #e1f1e5; }
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-service-requests-table th { background: #353331; }
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-service-requests-table th,
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-service-requests-table td { border-color: #484541; }
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-service-request-status { color: #dedbd7; background: #484541; }
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-service-request-status[data-status="OPEN"] { color: #b8e4c6; background: #214d34; }
@media (prefers-reduced-motion: reduce) {
.opportunities-extension-service-requests-table .opportunities-extension-account-plans-skeleton-bar { animation: none; }
}

View File

@@ -0,0 +1,121 @@
function createServiceRequestsView({ MODAL_ID, createCloseIcon, formatOracleResponseDate, environment }) {
const { document } = environment;
let active = null;
const columns = [
["name", "Name", item => item.customer?.name],
["registryId", "Registry ID", item => item.customer?.registryId],
["id", "SR Number", item => item.id],
["severity", "Severity", item => item.severity],
["status", "Status", item => item.status],
["subStatus", "Substatus", item => item.subStatus],
["productName", "Product Name", item => item.component?.productName],
["os", "OS", item => item.os], ["csi", "CSI", item => item.csi],
["created", "Creation date", item => item.created],
["lastUpdated", "Modified", item => item.lastUpdated]
];
function element(tag, className, text) {
const node = document.createElement(tag);
if (className) node.className = className;
if (text !== undefined) node.textContent = text;
return node;
}
function closeServiceRequestsModal() {
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-service-requests-table");
table.setAttribute("aria-label", "Customer service requests");
const group = element("colgroup");
const head = element("thead");
const header = element("tr");
columns.forEach(([key, label]) => {
group.append(element("col", `service-requests-col-${key}`));
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, , getValue]) => {
const cell = element("td");
cell.dataset.field = key;
if (loading) {
const bar = element("span", "opportunities-extension-account-plans-skeleton-bar");
bar.setAttribute("aria-hidden", "true");
cell.append(bar);
} else {
const value = getValue(item);
let text = value === null || value === undefined || value === "" ? "-" : String(value);
if (text !== "-" && (key === "created" || key === "lastUpdated")) text = formatOracleResponseDate(value);
if (key === "status") {
const badge = element("span", "opportunities-extension-service-request-status", text);
badge.dataset.status = text.trim().toUpperCase();
cell.append(badge);
} else cell.textContent = text;
}
row.append(cell);
});
body.append(row);
});
table.append(group, head, body);
return table;
}
function openServiceRequestsModal(opportunity, trigger) {
closeServiceRequestsModal();
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-service-requests-title");
const dialog = element("div", "opportunities-extension-account-plans-dialog opportunities-extension-service-requests-dialog");
const header = element("header", "opportunities-extension-account-plans-header");
const heading = element("div");
const title = element("h2", "", "Service Requests");
title.id = "opportunities-extension-service-requests-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 service requests");
close.append(createCloseIcon());
close.addEventListener("click", closeServiceRequestsModal);
header.append(heading, close);
const content = element("div", "opportunities-extension-service-requests-content");
content.tabIndex = 0;
content.setAttribute("role", "region");
content.setAttribute("aria-label", "Service requests results");
const status = element("p", "opportunities-extension-service-requests-message", "Loading service requests...");
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 = "") {
if (active?.modal !== modal || overlay.hidden) return;
modal.setAttribute("aria-busy", "false");
content.textContent = "";
status.textContent = error || (items.length ? `${items.length} service requests` : "No open service requests found for this customer.");
status.setAttribute("role", error ? "alert" : "status");
content.append(status);
if (!error && items.length) content.append(createTable(items));
} };
}
return { openServiceRequestsModal, closeServiceRequestsModal, createTable };
}