Implemented the Plan Detail view to display consumption overages or forfeitures
This commit is contained in:
@@ -113,10 +113,11 @@ function createBackgroundApplication(overrides = {}) {
|
||||
const requestCustomerServiceRequest = (...args) => spaCustomerServiceRequests.requestCustomerServiceRequest(...args);
|
||||
const requestConsumerTokenService = (...args) => authConsumerSession.requestConsumerTokenService(...args);
|
||||
const requestConsumptionTrend = (...args) => spaConsumption.requestConsumptionTrend(...args);
|
||||
const requestConsumptionPlanDetail = (...args) => spaConsumption.requestConsumptionPlanDetail(...args);
|
||||
const requestOpptyProducts = (...args) => productsBackgroundJobs.requestOpptyProducts(...args);
|
||||
const runtimeApi = environment.browser || environment.chrome;
|
||||
function start() {
|
||||
startBackgroundApplication({ messageTypes, getXsrfTokenCookie, refreshXsrfCookie, requestAccountPlans, requestInstallBase, requestCustomerServiceRequest, requestConsumerTokenService, requestConsumptionTrend, requestOpptyProducts, runtimeApi });
|
||||
startBackgroundApplication({ messageTypes, getXsrfTokenCookie, refreshXsrfCookie, requestAccountPlans, requestInstallBase, requestCustomerServiceRequest, requestConsumerTokenService, requestConsumptionTrend, requestConsumptionPlanDetail, requestOpptyProducts, runtimeApi });
|
||||
}
|
||||
return {
|
||||
start, contracts,
|
||||
|
||||
@@ -8,6 +8,7 @@ function registerBackgroundMessages({
|
||||
requestCustomerServiceRequest,
|
||||
requestConsumerTokenService,
|
||||
requestConsumptionTrend,
|
||||
requestConsumptionPlanDetail,
|
||||
requestOpptyProducts,
|
||||
runtimeApi
|
||||
}) {
|
||||
@@ -115,6 +116,14 @@ runtimeApi.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (message.type === messageTypes.requestConsumptionPlanDetail) {
|
||||
requestConsumptionPlanDetail(message).then(sendResponse).catch(error => sendResponse({
|
||||
ok: false, error: error.message || "Unable to load consumption plan details.",
|
||||
debugRequests: Array.isArray(error.debugRequests) ? error.debugRequests : []
|
||||
}));
|
||||
return true;
|
||||
}
|
||||
|
||||
if (message.type === messageTypes.requestConsumptionTrend) {
|
||||
requestConsumptionTrend(message)
|
||||
.then(sendResponse)
|
||||
|
||||
@@ -361,12 +361,26 @@ function createContentApplication(overrides = {}) {
|
||||
showCopyConfirmation: (...args) => opportunitiesLinks.showCopyConfirmation(...args),
|
||||
environment
|
||||
});
|
||||
const consumptionPlanDetailView = createConsumptionPlanDetailView({
|
||||
createCloseIcon: (...args) => contentIcons.createCloseIcon(...args),
|
||||
environment
|
||||
});
|
||||
const consumptionPlanDetailController = createConsumptionPlanDetailController({
|
||||
messageTypes,
|
||||
requestConsumerTokenServiceOnce: (...args) => authContentSession.requestConsumerTokenServiceOnce(...args),
|
||||
sendRuntimeMessage: (...args) => platformContentMessages.sendRuntimeMessage(...args),
|
||||
appendBackgroundRequestLog: (...args) => debugContentLog.appendBackgroundRequestLog(...args),
|
||||
renderPlanDetailBadges: (...args) => consumptionPlanDetailView.renderPlanDetailBadges(...args),
|
||||
environment
|
||||
});
|
||||
const consumptionPlansView = createConsumptionPlansView({
|
||||
ACCOUNT_PLANS_MODAL_ID: contracts.ACCOUNT_PLANS_MODAL_ID,
|
||||
DEFAULT_CONSUMPTION_TREND_PERIOD: contracts.DEFAULT_CONSUMPTION_TREND_PERIOD,
|
||||
MODAL_ID: contracts.MODAL_ID,
|
||||
createCloseIcon: (...args) => contentIcons.createCloseIcon(...args),
|
||||
createConsumptionTrendSection: (...args) => consumptionTrendController.createConsumptionTrendSection(...args),
|
||||
loadPlanDetailForPanel: (...args) => consumptionPlanDetailController.loadPlanDetailForPanel(...args),
|
||||
closePlanDetailPopover: (...args) => consumptionPlanDetailView.closePlanDetailPopover(...args),
|
||||
createSvgElement: (...args) => consumptionChart.createSvgElement(...args),
|
||||
formatOracleResponseDate: (...args) => opportunitiesFormatters.formatOracleResponseDate(...args),
|
||||
getNestedValue: (...args) => opportunitiesFormatters.getNestedValue(...args),
|
||||
@@ -374,6 +388,7 @@ function createContentApplication(overrides = {}) {
|
||||
environment
|
||||
});
|
||||
const consumptionTrendController = createConsumptionTrendController({
|
||||
createPlanDetailSection: (...args) => consumptionPlanDetailController.createPlanDetailSection(...args),
|
||||
messageTypes,
|
||||
CONSUMPTION_TREND_PERIODS: contracts.CONSUMPTION_TREND_PERIODS,
|
||||
DEFAULT_CONSUMPTION_TREND_PERIOD: contracts.DEFAULT_CONSUMPTION_TREND_PERIOD,
|
||||
@@ -462,6 +477,8 @@ function createContentApplication(overrides = {}) {
|
||||
serviceRequestsAction,
|
||||
serviceRequestsView,
|
||||
consumptionPlansView,
|
||||
consumptionPlanDetailController,
|
||||
consumptionPlanDetailView,
|
||||
consumptionTrendController,
|
||||
consumptionChart,
|
||||
consumptionModel,
|
||||
|
||||
63
src/features/consumption/plan-detail-controller.js
Normal file
63
src/features/consumption/plan-detail-controller.js
Normal file
@@ -0,0 +1,63 @@
|
||||
function createConsumptionPlanDetailController({ messageTypes, requestConsumerTokenServiceOnce, sendRuntimeMessage,
|
||||
appendBackgroundRequestLog, renderPlanDetailBadges, environment }) {
|
||||
const { document } = environment;
|
||||
const states = new WeakMap();
|
||||
const pending = new Map();
|
||||
|
||||
function createPlanDetailSection(plan) {
|
||||
const section = document.createElement("div");
|
||||
section.className = "opportunities-extension-plan-detail-badges";
|
||||
section.dataset.subPlanNum = String(plan.subPlanNum || "").trim();
|
||||
section.setAttribute("aria-live", "polite");
|
||||
states.set(section, { status: "idle" });
|
||||
return section;
|
||||
}
|
||||
async function request(subPlanNum) {
|
||||
const authorizations = await requestConsumerTokenServiceOnce();
|
||||
const response = await sendRuntimeMessage({ type: messageTypes.requestConsumptionPlanDetail, subPlanNum,
|
||||
authorization: authorizations && authorizations.accountPlans });
|
||||
appendBackgroundRequestLog(response?.debugRequests);
|
||||
if (!response?.ok) throw new Error(response?.error || "Unable to load consumption plan details.");
|
||||
return response.items;
|
||||
}
|
||||
function loadPlanDetailForPanel(panel, plan) {
|
||||
const section = panel?.querySelector(".opportunities-extension-plan-detail-badges");
|
||||
if (!section) return Promise.resolve();
|
||||
const state = states.get(section);
|
||||
if (state?.status === "ready") return Promise.resolve();
|
||||
if (state?.status === "loading") return state.promise;
|
||||
const subPlanNum = String(plan.subPlanNum || "").trim();
|
||||
section.textContent = "";
|
||||
section.setAttribute("aria-busy", "true");
|
||||
const skeleton = document.createElement("span");
|
||||
skeleton.className = "opportunities-extension-account-plans-skeleton-bar opportunities-extension-plan-detail-skeleton";
|
||||
skeleton.setAttribute("role", "status");
|
||||
skeleton.setAttribute("aria-label", "Loading plan projection");
|
||||
section.append(skeleton);
|
||||
const current = { status: "loading", promise: null };
|
||||
states.set(section, current);
|
||||
current.promise = (async () => {
|
||||
try {
|
||||
if (!subPlanNum) throw new Error("Sub plan number is unavailable.");
|
||||
if (!pending.has(subPlanNum)) pending.set(subPlanNum, request(subPlanNum).finally(() => pending.delete(subPlanNum)));
|
||||
const items = await pending.get(subPlanNum);
|
||||
if (!section.isConnected || states.get(section) !== current) return;
|
||||
current.status = "ready";
|
||||
renderPlanDetailBadges(section, items);
|
||||
} catch (error) {
|
||||
if (!section.isConnected || states.get(section) !== current) return;
|
||||
current.status = "error";
|
||||
section.textContent = "";
|
||||
const notice = document.createElement("span");
|
||||
notice.className = "opportunities-extension-plan-detail-error";
|
||||
notice.setAttribute("role", "alert");
|
||||
notice.textContent = error.message || "Unable to load plan details.";
|
||||
section.append(notice);
|
||||
} finally {
|
||||
if (states.get(section) === current) section.setAttribute("aria-busy", "false");
|
||||
}
|
||||
})();
|
||||
return current.promise;
|
||||
}
|
||||
return { createPlanDetailSection, loadPlanDetailForPanel };
|
||||
}
|
||||
160
src/features/consumption/plan-detail-view.js
Normal file
160
src/features/consumption/plan-detail-view.js
Normal file
@@ -0,0 +1,160 @@
|
||||
function createConsumptionPlanDetailView({ createCloseIcon, environment }) {
|
||||
const { document, window, setTimeout, clearTimeout } = environment;
|
||||
let active = null;
|
||||
let sequence = 0;
|
||||
const fields = [
|
||||
["pricingModel", "Pricing model"], ["lineStatus", "Line status"],
|
||||
["startDate", "Start date"], ["endDate", "End date"],
|
||||
["committedCredits", "Committed credits", true], ["consumed", "Consumed", true],
|
||||
["consumptionL30Days", "Consumption last 30 days", true], ["monthsRemaining", "Months remaining"],
|
||||
["projectedConsumption", "Projected consumption", true], ["projectedDelta", "Projected delta", true],
|
||||
["orderId", "Order number"]
|
||||
];
|
||||
function node(tag, className, text) {
|
||||
const result = document.createElement(tag);
|
||||
if (className) result.className = className;
|
||||
if (text !== undefined) result.textContent = text;
|
||||
return result;
|
||||
}
|
||||
function statusBadge(record, tag = "span") {
|
||||
const text = String(record.projectedStatus || "Unknown");
|
||||
const badge = node(tag, "opportunities-extension-projection-badge", text);
|
||||
badge.dataset.status = text.trim().toLowerCase();
|
||||
return badge;
|
||||
}
|
||||
function orderLink(record, text) {
|
||||
try {
|
||||
const url = new URL(record.orderTrackerURL);
|
||||
if (url.origin !== "https://accountdashboard.oracle.com" || url.username || url.password) return null;
|
||||
const link = node("a", "", text);
|
||||
link.href = url.href;
|
||||
link.target = "_blank";
|
||||
link.rel = "noopener noreferrer";
|
||||
return link;
|
||||
} catch { return null; }
|
||||
}
|
||||
function createLineDetails(record) {
|
||||
const section = node("section", "opportunities-extension-projection-line");
|
||||
const heading = node("h3");
|
||||
heading.append(statusBadge(record));
|
||||
section.append(heading);
|
||||
const details = node("dl", "opportunities-extension-projection-fields");
|
||||
fields.forEach(([key, label, numeric]) => {
|
||||
const raw = record[key];
|
||||
let text = raw === null || raw === undefined || raw === "" ? "-" : String(raw);
|
||||
if (numeric && text !== "-" && Number.isFinite(Number(raw))) {
|
||||
text = new Intl.NumberFormat("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 }).format(Number(raw));
|
||||
}
|
||||
const group = node("div");
|
||||
const value = node("dd");
|
||||
const link = key === "orderId" && text !== "-" ? orderLink(record, text) : null;
|
||||
if (link) value.append(link);
|
||||
else value.textContent = text;
|
||||
group.append(node("dt", "", label), value);
|
||||
details.append(group);
|
||||
});
|
||||
section.append(details);
|
||||
if (Array.isArray(record.clines) && record.clines.length) {
|
||||
const children = node("div", "opportunities-extension-projection-children");
|
||||
record.clines.forEach(child => children.append(createLineDetails(child)));
|
||||
section.append(children);
|
||||
}
|
||||
return section;
|
||||
}
|
||||
function closePlanDetailPopover(restoreFocus = false) {
|
||||
if (!active) return;
|
||||
const previous = active;
|
||||
active = null;
|
||||
clearTimeout(previous.timer);
|
||||
previous.popover.hidden = true;
|
||||
previous.button.setAttribute("aria-expanded", "false");
|
||||
document.removeEventListener("pointerdown", previous.outside, true);
|
||||
window.removeEventListener("resize", previous.resize);
|
||||
if (restoreFocus && previous.button.isConnected) {
|
||||
previous.suppressFocus = true;
|
||||
previous.button.focus();
|
||||
previous.suppressFocus = false;
|
||||
}
|
||||
}
|
||||
function positionPopover(entry) {
|
||||
const rect = entry.button.getBoundingClientRect();
|
||||
const viewportWidth = window.innerWidth;
|
||||
const viewportHeight = window.innerHeight;
|
||||
const width = Math.min(560, viewportWidth - 24);
|
||||
const height = Math.min(520, viewportHeight - 24);
|
||||
entry.popover.style.width = `${width}px`;
|
||||
entry.popover.style.maxHeight = `${height}px`;
|
||||
const below = viewportHeight - rect.bottom - 12;
|
||||
const actualHeight = Math.min(entry.popover.getBoundingClientRect().height, height);
|
||||
entry.popover.style.left = `${Math.max(12, Math.min(rect.left, viewportWidth - width - 12))}px`;
|
||||
entry.popover.style.top = `${Math.max(12, below >= actualHeight ? rect.bottom + 4 : Math.min(rect.top - actualHeight - 4, viewportHeight - actualHeight - 12))}px`;
|
||||
}
|
||||
function createInteractiveBadge(record) {
|
||||
const wrapper = node("div", "opportunities-extension-projection-wrapper");
|
||||
const button = statusBadge(record, "button");
|
||||
button.type = "button";
|
||||
button.setAttribute("aria-label", `Projected status: ${button.textContent}. View plan details`);
|
||||
button.setAttribute("aria-haspopup", "dialog");
|
||||
button.setAttribute("aria-expanded", "false");
|
||||
const popover = node("div", "opportunities-extension-projection-popover");
|
||||
popover.id = `opportunities-extension-projection-${++sequence}`;
|
||||
popover.hidden = true;
|
||||
popover.setAttribute("role", "dialog");
|
||||
popover.setAttribute("aria-label", "Consumption plan details");
|
||||
popover.tabIndex = -1;
|
||||
button.setAttribute("aria-controls", popover.id);
|
||||
const dismiss = node("button", "opportunities-extension-projection-dismiss");
|
||||
dismiss.type = "button";
|
||||
dismiss.setAttribute("aria-label", "Close plan details");
|
||||
dismiss.title = "Close plan details";
|
||||
dismiss.append(createCloseIcon());
|
||||
dismiss.addEventListener("click", () => closePlanDetailPopover(true));
|
||||
popover.append(dismiss, createLineDetails(record));
|
||||
const entry = { button, popover, wrapper, pinned: false, timer: null, suppressFocus: false,
|
||||
outside: event => { if (!wrapper.contains(event.target)) closePlanDetailPopover(); },
|
||||
resize: () => positionPopover(entry) };
|
||||
function open() {
|
||||
if (active !== entry) {
|
||||
closePlanDetailPopover();
|
||||
entry.pinned = false;
|
||||
active = entry;
|
||||
document.addEventListener("pointerdown", entry.outside, true);
|
||||
window.addEventListener("resize", entry.resize);
|
||||
}
|
||||
clearTimeout(entry.timer);
|
||||
popover.hidden = false;
|
||||
button.setAttribute("aria-expanded", "true");
|
||||
positionPopover(entry);
|
||||
}
|
||||
wrapper.addEventListener("pointerenter", open);
|
||||
wrapper.addEventListener("pointerleave", () => {
|
||||
if (entry.pinned || wrapper.contains(document.activeElement)) return;
|
||||
entry.timer = setTimeout(() => { if (active === entry) closePlanDetailPopover(); }, 180);
|
||||
});
|
||||
wrapper.addEventListener("focusin", () => { if (!entry.suppressFocus) open(); });
|
||||
wrapper.addEventListener("focusout", event => { if (!wrapper.contains(event.relatedTarget) && active === entry) closePlanDetailPopover(); });
|
||||
button.addEventListener("click", () => {
|
||||
if (active === entry && entry.pinned) closePlanDetailPopover();
|
||||
else { open(); entry.pinned = true; }
|
||||
});
|
||||
wrapper.addEventListener("keydown", event => {
|
||||
if (event.key === "Escape" && active === entry) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
closePlanDetailPopover(true);
|
||||
}
|
||||
});
|
||||
wrapper.append(button, popover);
|
||||
return wrapper;
|
||||
}
|
||||
function renderPlanDetailBadges(section, items) {
|
||||
if (active && section.contains(active.button)) closePlanDetailPopover();
|
||||
section.textContent = "";
|
||||
if (!items.length) {
|
||||
section.append(node("span", "opportunities-extension-projection-empty", "No projection data"));
|
||||
return;
|
||||
}
|
||||
items.forEach(record => section.append(createInteractiveBadge(record)));
|
||||
}
|
||||
return { renderPlanDetailBadges, createLineDetails, closePlanDetailPopover };
|
||||
}
|
||||
47
src/features/consumption/plan-detail.css
Normal file
47
src/features/consumption/plan-detail.css
Normal file
@@ -0,0 +1,47 @@
|
||||
.opportunities-extension-plan-detail-badges { display: flex; align-items: center; flex-wrap: wrap; gap: 6px; }
|
||||
.opportunities-extension-consumption-trend-header { align-items: center; justify-content: space-between; gap: 12px; }
|
||||
.opportunities-extension-consumption-trend-header > .opportunities-extension-plan-detail-badges { min-width: 0; }
|
||||
.opportunities-extension-plan-detail-skeleton { width: 64px; height: 20px; }
|
||||
.opportunities-extension-projection-badge {
|
||||
display: inline-flex; align-items: center; justify-content: center; border: 1px solid transparent;
|
||||
padding: 3px 8px; border-radius: 12px; font-size: 11px; font-weight: 600; line-height: 1.3;
|
||||
color: #5f5a55; background: #eceae8; white-space: nowrap;
|
||||
}
|
||||
button.opportunities-extension-projection-badge { cursor: pointer; }
|
||||
.opportunities-extension-projection-badge[data-status="forfeit"] { color: #a12424; background: #fbe5e5; }
|
||||
.opportunities-extension-projection-badge[data-status="overage"] { color: #185a9d; background: #e0edfc; }
|
||||
.opportunities-extension-projection-badge:focus-visible { outline: 2px solid currentColor; outline-offset: 2px; }
|
||||
.opportunities-extension-projection-popover {
|
||||
position: fixed; z-index: 8; box-sizing: border-box; padding: 14px; overflow: auto; overscroll-behavior: contain;
|
||||
border: 1px solid #b9b4ae; border-radius: 4px; background: #ffffff; color: #312d2a;
|
||||
box-shadow: 0 4px 16px #0003; font-size: 12px; line-height: 1.4; overflow-wrap: anywhere;
|
||||
}
|
||||
.opportunities-extension-projection-popover[hidden] { display: none; }
|
||||
.opportunities-extension-projection-popover .opportunities-extension-projection-dismiss {
|
||||
display: grid; place-items: center; width: 24px; height: 24px; margin-left: auto; margin-bottom: 8px; padding: 4px; border: 1px solid #b9b4ae;
|
||||
border-radius: 4px; background: transparent; color: inherit; cursor: pointer; font-size: 12px;
|
||||
}
|
||||
.opportunities-extension-projection-dismiss svg { width: 14px; height: 14px; }
|
||||
.opportunities-extension-projection-line h3 { margin: 0 0 8px; font-size: 12px; }
|
||||
.opportunities-extension-projection-fields { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px 16px; margin: 0; }
|
||||
.opportunities-extension-projection-fields > div { min-width: 0; }
|
||||
.opportunities-extension-projection-fields dt { font-weight: 600; color: #5f5a55; font-size: 11px; }
|
||||
.opportunities-extension-projection-fields dd { margin: 2px 0 0; font-variant-numeric: tabular-nums; }
|
||||
.opportunities-extension-projection-fields a { color: #006d7a; text-decoration: underline; }
|
||||
.opportunities-extension-projection-children { margin-top: 14px; padding-left: 12px; border-left: 2px solid #dedbd7; }
|
||||
.opportunities-extension-projection-children > section + section { margin-top: 16px; }
|
||||
.opportunities-extension-plan-detail-error { color: #a12424; font-size: 12px; }
|
||||
.opportunities-extension-projection-empty { color: #5f5a55; font-size: 12px; }
|
||||
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-projection-popover { color: #f1efec; background: #302e2b; border-color: #716b64; }
|
||||
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-projection-badge { color: #dedbd7; background: #484541; }
|
||||
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-projection-badge[data-status="forfeit"] { color: #ffb9b9; background: #682e32; }
|
||||
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-projection-badge[data-status="overage"] { color: #c5dfff; background: #224b76; }
|
||||
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-projection-fields dt,
|
||||
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-projection-empty { color: #cec9c2; }
|
||||
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-projection-fields a { color: #76dbea; }
|
||||
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-plan-detail-error { color: #ffb9b9; }
|
||||
@media (max-width: 480px) { .opportunities-extension-projection-fields { grid-template-columns: minmax(0, 1fr); } }
|
||||
@media (max-width: 480px) {
|
||||
.opportunities-extension-consumption-trend-header > .opportunities-extension-consumption-periods { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
||||
}
|
||||
@media (prefers-reduced-motion: reduce) { .opportunities-extension-plan-detail-skeleton { animation: none; } }
|
||||
@@ -4,6 +4,8 @@ function createConsumptionPlansView({
|
||||
MODAL_ID,
|
||||
createCloseIcon,
|
||||
createConsumptionTrendSection,
|
||||
loadPlanDetailForPanel,
|
||||
closePlanDetailPopover,
|
||||
createSvgElement,
|
||||
formatOracleResponseDate,
|
||||
getNestedValue,
|
||||
@@ -28,6 +30,7 @@ function createConsumptionPlansView({
|
||||
return;
|
||||
}
|
||||
|
||||
closePlanDetailPopover();
|
||||
existingModal?.remove();
|
||||
|
||||
const modal = document.createElement("section");
|
||||
@@ -119,6 +122,7 @@ function createConsumptionPlansView({
|
||||
panel.append(createAccountPlanDetails(plan), createConsumptionTrendSection(plan));
|
||||
|
||||
tab.addEventListener("click", () => {
|
||||
closePlanDetailPopover();
|
||||
tabList.querySelectorAll("[role='tab']").forEach((candidate) => {
|
||||
const isSelected = candidate === tab;
|
||||
candidate.setAttribute("aria-selected", String(isSelected));
|
||||
@@ -133,6 +137,7 @@ function createConsumptionPlansView({
|
||||
plan,
|
||||
trendSection ? trendSection.dataset.period : DEFAULT_CONSUMPTION_TREND_PERIOD
|
||||
);
|
||||
loadPlanDetailForPanel(panel, plan);
|
||||
});
|
||||
tabList.append(tab);
|
||||
panels.append(panel);
|
||||
@@ -151,6 +156,7 @@ function createConsumptionPlansView({
|
||||
|
||||
if (firstPanel) {
|
||||
loadConsumptionTrendForPanel(firstPanel, plans[0], DEFAULT_CONSUMPTION_TREND_PERIOD);
|
||||
loadPlanDetailForPanel(firstPanel, plans[0]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -347,6 +353,7 @@ function createConsumptionPlansView({
|
||||
}
|
||||
|
||||
function closeAccountPlansModal() {
|
||||
closePlanDetailPopover();
|
||||
const modal = document.getElementById(ACCOUNT_PLANS_MODAL_ID);
|
||||
|
||||
if (modal) {
|
||||
|
||||
@@ -4,6 +4,7 @@ function createConsumptionTrendController({
|
||||
DEFAULT_CONSUMPTION_TREND_PERIOD,
|
||||
appendBackgroundRequestLog,
|
||||
consumptionTrendState,
|
||||
createPlanDetailSection,
|
||||
renderConsumptionTrendChart,
|
||||
requestConsumerTokenServiceOnce,
|
||||
sendRuntimeMessage,
|
||||
@@ -44,7 +45,7 @@ function createConsumptionTrendController({
|
||||
const chart = document.createElement("div");
|
||||
chart.className = "opportunities-extension-consumption-chart";
|
||||
chart.setAttribute("aria-live", "polite");
|
||||
header.append(periodControls);
|
||||
header.append(createPlanDetailSection(plan), periodControls);
|
||||
section.append(header, chart);
|
||||
return section;
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ function createDebugContentView({
|
||||
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";
|
||||
if (/^request(?:AccountSummary|AccountPlans|ConsumptionService|ConsumptionPlanDetail)/.test(entry.name)) return "consumption";
|
||||
return "other";
|
||||
}
|
||||
|
||||
|
||||
@@ -54,5 +54,30 @@ function createSpaConsumption({
|
||||
}
|
||||
}
|
||||
|
||||
return { requestConsumptionTrend };
|
||||
async function requestConsumptionPlanDetail(message) {
|
||||
const debugRequests = [];
|
||||
try {
|
||||
const subPlanNum = typeof message.subPlanNum === "string" ? message.subPlanNum.trim() : "";
|
||||
const authorization = normalizeMessageAuthorization(message.authorization, "g2m-authorization");
|
||||
if (!subPlanNum) throw new Error("Sub plan number is required for plan details.");
|
||||
if (!authorization.value) throw new Error("OCICONS-TS authorization is unavailable for plan details.");
|
||||
const url = `https://spa.oracle.com/oalcrm/web/api/provider-proxy/g2m-consumption/service/v1/plan/plandetail/${encodeURIComponent(subPlanNum)}?active=true`;
|
||||
const items = await fetchJson(url, {
|
||||
method: "GET", credentials: "include", cache: "no-store",
|
||||
headers: { Accept: "application/json", "G2m-Authorization": authorization.value }
|
||||
}, "requestConsumptionPlanDetail", debugRequests);
|
||||
const validLines = (lines, depth = 0) => Array.isArray(lines) && depth <= 20 && lines.every(line =>
|
||||
line && typeof line === "object" && !Array.isArray(line)
|
||||
&& (line.clines === undefined || line.clines === null || validLines(line.clines, depth + 1)));
|
||||
if (!validLines(items)) throw new Error("requestConsumptionPlanDetail returned invalid plan details.");
|
||||
return { ok: true, subPlanNum, items, debugRequests };
|
||||
} catch (error) {
|
||||
const entry = debugRequests[debugRequests.length - 1];
|
||||
if (entry && entry.status === "success") { entry.status = "failed"; entry.error = error.message; }
|
||||
error.debugRequests = debugRequests;
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
return { requestConsumptionTrend, requestConsumptionPlanDetail };
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ function createMessageTypes() {
|
||||
requestAccountPlans: "opportunitiesExtension.requestAccountPlans",
|
||||
requestInstallBase: "opportunitiesExtension.requestInstallBase",
|
||||
requestCustomerServiceRequest: "opportunitiesExtension.requestCustomerServiceRequest",
|
||||
requestConsumptionTrend: "opportunitiesExtension.requestConsumptionTrend"
|
||||
requestConsumptionTrend: "opportunitiesExtension.requestConsumptionTrend",
|
||||
requestConsumptionPlanDetail: "opportunitiesExtension.requestConsumptionPlanDetail"
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user