161 lines
7.3 KiB
JavaScript
161 lines
7.3 KiB
JavaScript
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 };
|
|
}
|