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 }; }