176 lines
10 KiB
JavaScript
176 lines
10 KiB
JavaScript
function createOpportunityWorkloadActions({ authSessionState, allowsCrmActivities, requestConsumerTokenServiceOnce, sendRuntimeMessage,
|
|
appendBackgroundRequestLog, queueProductsTableRender, productsContentState, messageTypes, reportProgress, activityCache, createActionButton, closeActionForm, setBadgeContent, environment }) {
|
|
let session = newSession();
|
|
function newSession() { return { enabled: true, records: new Map(), queue: [], running: 0, progress: new Set() }; }
|
|
function updateProgress(current) {
|
|
if (current !== session || !current.enabled || !current.progress.size) return;
|
|
const records = [...current.progress];
|
|
const completed = records.filter(record => record.status !== "loading").length;
|
|
const failed = records.filter(record => record.status === "error").length;
|
|
reportProgress(completed < records.length ? "running" : failed ? "complete-with-errors" : "complete", completed, records.length);
|
|
}
|
|
function reset(enabled = true) {
|
|
closeActionForm(false);
|
|
session.enabled = false;
|
|
for (const job of session.queue) job.record.resolveCompletion();
|
|
session.queue.length = 0;
|
|
session = newSession();
|
|
session.enabled = enabled;
|
|
reportProgress("idle");
|
|
}
|
|
function idsFor(products) {
|
|
return [...new Set((products?.items || []).map(item => String(item?.RevnId ?? "").trim()).filter(Boolean))];
|
|
}
|
|
function loadProducts(products, refresh = false, cacheOptions = null) {
|
|
if (!session.enabled || !allowsCrmActivities()) return;
|
|
const pending = [];
|
|
if ([...session.progress].every(record => record.status !== "loading")) session.progress.clear();
|
|
for (const revnId of idsFor(products)) {
|
|
if (!refresh && session.records.has(revnId)) {
|
|
pending.push(session.records.get(revnId).completion);
|
|
continue;
|
|
}
|
|
session.progress.delete(session.records.get(revnId));
|
|
const record = { status: "loading", actions: [], hasNext: false };
|
|
record.completion = new Promise(resolve => { record.resolveCompletion = resolve; });
|
|
pending.push(record.completion);
|
|
session.records.set(revnId, record);
|
|
if (!cacheOptions) session.progress.add(record);
|
|
session.queue.push({ revnId, record, cacheOptions });
|
|
}
|
|
updateProgress(session);
|
|
pump(session);
|
|
return Promise.all(pending);
|
|
}
|
|
function reopen() {
|
|
reset();
|
|
for (const products of productsContentState.opptyProductsByNumber.values()) loadProducts(products);
|
|
queueProductsTableRender();
|
|
}
|
|
function blockProducts(products) {
|
|
if (!allowsCrmActivities()) return;
|
|
for (const id of idsFor(products)) {
|
|
session.records.set(id, { status: "error", actions: [], hasNext: false, error: "CRM service requests unavailable." });
|
|
}
|
|
updateProgress(session);
|
|
}
|
|
function pump(current) {
|
|
while (current.enabled && current === session && current.running < 4 && current.queue.length) {
|
|
const job = current.queue.shift();
|
|
current.running += 1;
|
|
run(current, job).finally(() => { current.running -= 1; pump(current); });
|
|
}
|
|
}
|
|
async function run(current, { revnId, record, cacheOptions }) {
|
|
try {
|
|
const cached = cacheOptions ? await activityCache.read("actions", revnId, cacheOptions.cachedAt) : null;
|
|
if (!current.enabled || current !== session || current.records.get(revnId) !== record || !allowsCrmActivities()) return;
|
|
if (cached) {
|
|
record.actions = cached.items;
|
|
record.hasNext = cached.hasNext;
|
|
record.status = "ready";
|
|
return;
|
|
}
|
|
if (cacheOptions?.cacheOnly) { record.status = "error"; record.error = "No matching activity cache for this product update."; return; }
|
|
current.progress.add(record);
|
|
updateProgress(current);
|
|
let result;
|
|
for (let attempt = 1; attempt <= 5; attempt += 1) {
|
|
try {
|
|
if (!current.enabled || current !== session || current.records.get(revnId) !== record || !allowsCrmActivities()) return;
|
|
const authorizations = await requestConsumerTokenServiceOnce();
|
|
if (!current.enabled || current !== session || current.records.get(revnId) !== record || !allowsCrmActivities()) return;
|
|
result = await sendRuntimeMessage({ type: messageTypes.requestWlActions, revnId, authorization: authorizations?.workloadActions });
|
|
if (!current.enabled || current !== session || current.records.get(revnId) !== record || !allowsCrmActivities()) return;
|
|
appendBackgroundRequestLog(result?.debugRequests);
|
|
if (!result?.ok) throw new Error(result?.error || "Unable to load workload actions.");
|
|
break;
|
|
} catch (error) {
|
|
if (attempt === 5) throw error;
|
|
}
|
|
}
|
|
record.actions = result.actions;
|
|
record.hasNext = result.hasNext;
|
|
record.status = "ready";
|
|
if (cacheOptions) await activityCache.write("actions", revnId, cacheOptions.cachedAt, result.actions, result.hasNext);
|
|
} catch (error) {
|
|
if (!current.enabled || current !== session) return;
|
|
record.status = "error";
|
|
record.error = error.message || "Unable to load workload actions.";
|
|
} finally {
|
|
record.resolveCompletion();
|
|
if (current.enabled && current === session) { updateProgress(current); queueProductsTableRender(); }
|
|
}
|
|
}
|
|
function getSummary(optyNumber) {
|
|
if (!session.enabled || !allowsCrmActivities() || !optyNumber) return null;
|
|
const products = productsContentState.opptyProductsByNumber.get(String(optyNumber));
|
|
if (!products) return null;
|
|
const ids = idsFor(products);
|
|
const records = ids.map(id => session.records.get(id));
|
|
const actions = records.flatMap(record => record?.actions || []);
|
|
const pending = records.some(record => !record || record.status === "loading");
|
|
const uncertain = products.error || (products.items || []).some(item => !String(item?.RevnId ?? "").trim())
|
|
|| records.some(record => record?.status === "error" || record?.hasNext);
|
|
const principal = authSessionState.principal;
|
|
const owned = Boolean(principal?.trim()) && actions.some(action => typeof action.owner === "string"
|
|
&& action.owner.trim().toLowerCase() === principal.trim().toLowerCase());
|
|
const state = owned ? "matched" : pending ? "loading" : uncertain || !principal ? "error" : "missing";
|
|
return { state, actions, records, principal, pending, uncertain };
|
|
}
|
|
async function applySavedAction(optyNumber, revnId, response, submittedAction = {}) {
|
|
const products = productsContentState.opptyProductsByNumber.get(String(optyNumber));
|
|
const id = String(revnId);
|
|
if (!session.enabled || !allowsCrmActivities() || !idsFor(products).includes(id)) return false;
|
|
const returned = Array.isArray(response) ? response : Array.isArray(response?.actions) ? response.actions : [response?.action || response];
|
|
const saved = returned.filter(action => action && typeof action === "object" && action.id != null
|
|
&& (action.revenueId == null || String(action.revenueId) === id));
|
|
if (!saved.length) return false;
|
|
const previous = session.records.get(id);
|
|
const actions = [...(previous?.actions || [])];
|
|
for (const action of saved) {
|
|
// The POST can return a display name in owner, unlike the workload GET.
|
|
const ownerEmail = typeof action.ownerEmail === "string" && action.ownerEmail.trim()
|
|
? action.ownerEmail.trim() : submittedAction.ownerEmail || action.owner || "";
|
|
const normalized = { ...action, ownerEmail, owner: ownerEmail };
|
|
const index = actions.findIndex(item => String(item.id) === String(action.id));
|
|
if (index === -1) actions.push(normalized);
|
|
else actions[index] = { ...actions[index], ...normalized };
|
|
}
|
|
const record = { status: "ready", actions, hasNext: previous?.hasNext || false,
|
|
completion: Promise.resolve(), resolveCompletion() {} };
|
|
session.records.set(id, record);
|
|
previous?.resolveCompletion?.();
|
|
session.progress.delete(previous);
|
|
queueProductsTableRender();
|
|
if (products.cachedAt) await activityCache.write("actions", id, products.cachedAt, actions, record.hasNext);
|
|
return true;
|
|
}
|
|
function createBadge(optyNumber) {
|
|
const summary = getSummary(optyNumber);
|
|
if (!summary) return null;
|
|
const { state, actions, records, principal, pending, uncertain } = summary;
|
|
const badge = environment.document.createElement("span");
|
|
badge.className = "opportunities-extension-crm-sr-badge opportunities-extension-wl-actions-badge";
|
|
badge.setAttribute("data-state", state);
|
|
setBadgeContent(badge, "actions", state === "matched" || state === "missing" ? actions.length : state === "loading" ? "..." : "?");
|
|
badge.title = actions.length ? actions.map(action => [
|
|
`Action: ${action.name ?? "N/A"}`, `ID: ${action.id ?? "N/A"}`, `Status: ${action.status ?? "N/A"}`,
|
|
`Owner: ${action.owner || "N/A"}`, `Email: ${action.ownerEmail || "N/A"}`, `Workload: ${action.workloadName || "N/A"}`,
|
|
`Description: ${action.workloadDescription || "N/A"}`, `Start: ${action.startDate || "N/A"}`, `End: ${action.endDate || "N/A"}`
|
|
].join("\n")).join("\n\n") : state === "missing" ? "No actions found for this opportunity's products."
|
|
: state === "loading" ? "Loading workload actions..." : "Unable to confirm workload actions. Check product RevnId and Debug Requests.";
|
|
if (records.some(record => record?.hasNext)) badge.title += "\nAdditional actions exist; pagination is not enabled.";
|
|
const errors = [...new Set(records.filter(record => record?.error).map(record => record.error))];
|
|
if (errors.length) badge.title += `\n${errors.join("\n")}`;
|
|
if (!principal) badge.title += "\nTokenrelay principal is unavailable; ownership cannot be verified.";
|
|
else if (actions.length && state === "missing") badge.title += "\nNone of the returned actions belong to the signed-in principal.";
|
|
if (actions.length && (pending || uncertain)) badge.title += "\nSome workloads are still loading or could not be fully checked.";
|
|
badge.tabIndex = 0;
|
|
badge.setAttribute("aria-label", badge.title);
|
|
if (state === "missing") badge.append(createActionButton(String(optyNumber)));
|
|
return badge;
|
|
}
|
|
return { reset, reopen, loadProducts, blockProducts, createBadge, getSummary, applySavedAction };
|
|
}
|