5.5.2026 update
This commit is contained in:
@@ -16,6 +16,28 @@ import {
|
||||
resolveStateReadPath,
|
||||
} from "./lib/runtime-paths.mjs";
|
||||
import { parseArgs, splitCsv } from "./lib/cli-shared.mjs";
|
||||
import { quoteLiteral, toSqlList } from "./lib/sql-literals.mjs";
|
||||
import {
|
||||
SA_ATTACH_DEFAULT_ALL_STATUSES,
|
||||
SA_ATTACH_DEFAULT_CLUSTER,
|
||||
SA_ATTACH_DEFAULT_MIN_OPPORTUNITY_PROBABILITY,
|
||||
SA_ATTACH_DEFAULT_OPEN_STATUSES,
|
||||
SA_ATTACH_DEFAULT_PRODUCT_LOBS,
|
||||
SA_ATTACH_DEFAULT_REVENUE_TYPE_GROUPS,
|
||||
SA_ATTACH_DEFAULT_SALES_CREDIT_TYPE,
|
||||
SA_ATTACH_DEFAULT_SOLUTIONS,
|
||||
SA_ATTACH_DEFAULT_WON_STATUSES,
|
||||
applyCloseWindowIntentDefaults,
|
||||
applyCurrentWorkloadIntentDefaults,
|
||||
resolveCommercialClusterFilter,
|
||||
resolveCoverageLimit,
|
||||
resolveManagerEmailFilter,
|
||||
resolveManagerEmailListFilter,
|
||||
resolveOptionalClusterFilter,
|
||||
resolveOwnerEmailFilter,
|
||||
resolveResourceFilter,
|
||||
resolveSaAttachFilters,
|
||||
} from "./lib/sa-attach-filters.mjs";
|
||||
import {
|
||||
cookieHeaderFor,
|
||||
extractClientBin,
|
||||
@@ -38,8 +60,13 @@ import {
|
||||
buildWarnings,
|
||||
emitJson,
|
||||
mapRows,
|
||||
parseNumericValue,
|
||||
} from "./lib/output-contract.mjs";
|
||||
import {
|
||||
buildSrCoverageKey,
|
||||
hasUsableOpportunityId,
|
||||
summarizeCurrentTeamOpportunityCoverage,
|
||||
summarizeOrgWideRevenueLineCoverage,
|
||||
} from "./lib/coverage-summary.mjs";
|
||||
|
||||
const DEFAULT_BASE_URL = "https://salesintelligence-dv.oracle.com";
|
||||
const DEFAULT_WORKBOOK_URL =
|
||||
@@ -104,20 +131,6 @@ const DATASET_REGISTRY = {
|
||||
allowedScopes: [SCOPE_MY_TEAM],
|
||||
},
|
||||
};
|
||||
const SA_ATTACH_DEFAULT_CLUSTER = "Brazil";
|
||||
const SA_ATTACH_DEFAULT_SOLUTIONS = [
|
||||
"Cloud Infrastructure - Workloads",
|
||||
"Hardware - Bookings",
|
||||
"On-Premise Technology - Bookings",
|
||||
];
|
||||
const SA_ATTACH_DEFAULT_PRODUCT_LOBS = ["Cloud Infrastructure", "Hardware", "License"];
|
||||
const SA_ATTACH_DEFAULT_REVENUE_TYPE_GROUPS = ["NEW", "EXPANSION", "WORKLOAD"];
|
||||
const SA_ATTACH_DEFAULT_OPEN_STATUSES = ["Open"];
|
||||
const SA_ATTACH_DEFAULT_WON_STATUSES = ["Won", "Won Pending"];
|
||||
const SA_ATTACH_DEFAULT_ALL_STATUSES = [
|
||||
...SA_ATTACH_DEFAULT_OPEN_STATUSES,
|
||||
...SA_ATTACH_DEFAULT_WON_STATUSES,
|
||||
];
|
||||
const VIEW_SCOPE_REGISTRY = {
|
||||
"current-fiscal-year": { sourceName: SOURCE_DV_SE_TEAM, requiresMyTeam: true },
|
||||
"data-quality-session-context": { sourceName: SOURCE_DV_SE_TEAM, requiresMyTeam: true },
|
||||
@@ -136,21 +149,6 @@ const VIEW_SCOPE_REGISTRY = {
|
||||
"srs-by-resource-email": { sourceName: SOURCE_DV_SE_TEAM, requiresMyTeam: true },
|
||||
"won-and-open-by-se-opty": { sourceName: SOURCE_DV_SE_TEAM, requiresMyTeam: true },
|
||||
};
|
||||
const SA_ATTACH_DEFAULT_SALES_CREDIT_TYPE = "QUOTA";
|
||||
const SA_ATTACH_DEFAULT_MIN_OPPORTUNITY_PROBABILITY = 0;
|
||||
const SA_ATTACH_PRODUCT_LOB_ALIASES = {
|
||||
"cloud applications": ["Cloud Applications"],
|
||||
"cloud infrastructure": ["Cloud Infrastructure"],
|
||||
hardware: ["Hardware"],
|
||||
license: ["License"],
|
||||
"license and hardware": ["License", "Hardware"],
|
||||
};
|
||||
const SA_ATTACH_REVENUE_TYPE_GROUP_ALIASES = {
|
||||
new: "NEW",
|
||||
expansion: "EXPANSION",
|
||||
workload: "WORKLOAD",
|
||||
workloads: "WORKLOAD",
|
||||
};
|
||||
const OPPORTUNITY_DETAILS_COLUMNS = [
|
||||
{ name: "opportunityId", expr: '"Opportunity"."Opportunity ID"', kind: "text" },
|
||||
{ name: "opportunityHours", expr: '"Effort"."# Opportunity Hours"', kind: "scalar" },
|
||||
@@ -367,7 +365,6 @@ const WORKLOAD_MANAGER_RESOURCE_CLOSE_WINDOW_COLUMNS = [
|
||||
"resourceDetails",
|
||||
];
|
||||
|
||||
const TEAM_EMAIL_COLUMN_NAMES = Array.from({ length: 8 }, (_, index) => `seTeamLevel${index + 1}EmailAddress`);
|
||||
const SQL_NULL_TEXT = `CAST(NULL AS VARCHAR(${VARCHAR_SIZE}))`;
|
||||
const SQL_NULL_SCALAR = "CAST(NULL AS DOUBLE)";
|
||||
|
||||
@@ -2126,51 +2123,7 @@ const INTENT_CATALOG = {
|
||||
"List commercial opportunities/workloads in a close-date window and enrich them with current-team presales/SR coverage for the executing user when present. Org-wide opportunity rows are preserved even when no current-team resource matches.",
|
||||
view: "opportunities-close-window",
|
||||
parameters: [
|
||||
{
|
||||
name: "fromDate",
|
||||
cli: "--from-date",
|
||||
required: false,
|
||||
default: "today",
|
||||
description: "Start of the close-date window. Accepts YYYY-MM-DD, today, +7d, -3d, or today+7d.",
|
||||
},
|
||||
{
|
||||
name: "toDate",
|
||||
cli: "--to-date",
|
||||
required: false,
|
||||
default: "+7d",
|
||||
description: "End of the close-date window. Accepts YYYY-MM-DD, today, +7d, -3d, or today+7d.",
|
||||
},
|
||||
{
|
||||
name: "date",
|
||||
cli: "--date",
|
||||
required: false,
|
||||
description: "Exact close date. Shortcut for a one-day window.",
|
||||
},
|
||||
{
|
||||
name: "month",
|
||||
cli: "--month",
|
||||
required: false,
|
||||
description: "Calendar month in YYYY-MM format. Shortcut for a full-month window.",
|
||||
},
|
||||
{
|
||||
name: "quarter",
|
||||
cli: "--quarter",
|
||||
required: false,
|
||||
description: "Fiscal quarter in FY26-Q4 or Q4 FY26 format. Shortcut for a full fiscal-quarter window.",
|
||||
},
|
||||
{
|
||||
name: "week",
|
||||
cli: "--week",
|
||||
required: false,
|
||||
description: "Annual week shortcut anchored on June 1. Accepts 1..53, w1..w53, or week 1..week 53.",
|
||||
},
|
||||
{
|
||||
name: "weekYear",
|
||||
cli: "--week-year",
|
||||
required: false,
|
||||
default: "current June-cycle start year",
|
||||
description: "Cycle start year used with --week. For example, 2025 means the 2025-06-01 through 2026-05-31 cycle.",
|
||||
},
|
||||
...closeWindowDateParameters(),
|
||||
{
|
||||
name: "optyStatus",
|
||||
cli: "--opty-status",
|
||||
@@ -2205,51 +2158,7 @@ const INTENT_CATALOG = {
|
||||
required: true,
|
||||
description: "Oracle user identifier. Accepts resource.member or resource.member@oracle.com.",
|
||||
},
|
||||
{
|
||||
name: "fromDate",
|
||||
cli: "--from-date",
|
||||
required: false,
|
||||
default: "today",
|
||||
description: "Start of the close-date window. Accepts YYYY-MM-DD, today, +7d, -3d, or today+7d.",
|
||||
},
|
||||
{
|
||||
name: "toDate",
|
||||
cli: "--to-date",
|
||||
required: false,
|
||||
default: "+7d",
|
||||
description: "End of the close-date window. Accepts YYYY-MM-DD, today, +7d, -3d, or today+7d.",
|
||||
},
|
||||
{
|
||||
name: "date",
|
||||
cli: "--date",
|
||||
required: false,
|
||||
description: "Exact close date. Shortcut for a one-day window.",
|
||||
},
|
||||
{
|
||||
name: "month",
|
||||
cli: "--month",
|
||||
required: false,
|
||||
description: "Calendar month in YYYY-MM format. Shortcut for a full-month window.",
|
||||
},
|
||||
{
|
||||
name: "quarter",
|
||||
cli: "--quarter",
|
||||
required: false,
|
||||
description: "Fiscal quarter in FY26-Q4 or Q4 FY26 format. Shortcut for a full fiscal-quarter window.",
|
||||
},
|
||||
{
|
||||
name: "week",
|
||||
cli: "--week",
|
||||
required: false,
|
||||
description: "Annual week shortcut anchored on June 1. Accepts 1..53, w1..w53, or week 1..week 53.",
|
||||
},
|
||||
{
|
||||
name: "weekYear",
|
||||
cli: "--week-year",
|
||||
required: false,
|
||||
default: "current June-cycle start year",
|
||||
description: "Cycle start year used with --week. For example, 2025 means the 2025-06-01 through 2026-05-31 cycle.",
|
||||
},
|
||||
...closeWindowDateParameters(),
|
||||
{
|
||||
name: "optyStatus",
|
||||
cli: "--opty-status",
|
||||
@@ -2283,51 +2192,7 @@ const INTENT_CATALOG = {
|
||||
required: true,
|
||||
description: "Manager email to match in the coverage hierarchy. Accepts resource.manager or resource.manager@oracle.com.",
|
||||
},
|
||||
{
|
||||
name: "fromDate",
|
||||
cli: "--from-date",
|
||||
required: false,
|
||||
default: "today",
|
||||
description: "Start of the close-date window. Accepts YYYY-MM-DD, today, +7d, -3d, or today+7d.",
|
||||
},
|
||||
{
|
||||
name: "toDate",
|
||||
cli: "--to-date",
|
||||
required: false,
|
||||
default: "+7d",
|
||||
description: "End of the close-date window. Accepts YYYY-MM-DD, today, +7d, -3d, or today+7d.",
|
||||
},
|
||||
{
|
||||
name: "date",
|
||||
cli: "--date",
|
||||
required: false,
|
||||
description: "Exact close date. Shortcut for a one-day window.",
|
||||
},
|
||||
{
|
||||
name: "month",
|
||||
cli: "--month",
|
||||
required: false,
|
||||
description: "Calendar month in YYYY-MM format. Shortcut for a full-month window.",
|
||||
},
|
||||
{
|
||||
name: "quarter",
|
||||
cli: "--quarter",
|
||||
required: false,
|
||||
description: "Fiscal quarter in FY26-Q4 or Q4 FY26 format. Shortcut for a full fiscal-quarter window.",
|
||||
},
|
||||
{
|
||||
name: "week",
|
||||
cli: "--week",
|
||||
required: false,
|
||||
description: "Annual week shortcut anchored on June 1. Accepts 1..53, w1..w53, or week 1..week 53.",
|
||||
},
|
||||
{
|
||||
name: "weekYear",
|
||||
cli: "--week-year",
|
||||
required: false,
|
||||
default: "current June-cycle start year",
|
||||
description: "Cycle start year used with --week. For example, 2025 means the 2025-06-01 through 2026-05-31 cycle.",
|
||||
},
|
||||
...closeWindowDateParameters(),
|
||||
{
|
||||
name: "optyStatus",
|
||||
cli: "--opty-status",
|
||||
@@ -3199,28 +3064,6 @@ custom SQL may use placeholders like {{field:closeDate}} and {{macro:securityEnt
|
||||
`;
|
||||
}
|
||||
|
||||
function normalizeSaAttachProductLobs(values) {
|
||||
return Array.from(
|
||||
new Set(
|
||||
values.flatMap((value) => {
|
||||
const normalized = String(value || "").trim().toLowerCase();
|
||||
return SA_ATTACH_PRODUCT_LOB_ALIASES[normalized] || [String(value).trim()];
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function normalizeSaAttachRevenueTypeGroups(values) {
|
||||
return Array.from(
|
||||
new Set(
|
||||
values.map((value) => {
|
||||
const normalized = String(value || "").trim().toLowerCase();
|
||||
return SA_ATTACH_REVENUE_TYPE_GROUP_ALIASES[normalized] || String(value).trim().toUpperCase();
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
async function withConsoleLogRoutedToStderr(callback) {
|
||||
const originalLog = console.log;
|
||||
console.log = (...args) => console.error(...args);
|
||||
@@ -3307,14 +3150,6 @@ function hasFreshSessionContextCache(meta) {
|
||||
return Date.now() - capturedAtMs <= SESSION_CONTEXT_CACHE_MAX_AGE_MS;
|
||||
}
|
||||
|
||||
function quoteLiteral(value) {
|
||||
return `'${String(value).replace(/'/g, "''")}'`;
|
||||
}
|
||||
|
||||
function toSqlList(values) {
|
||||
return values.map(quoteLiteral).join(", ");
|
||||
}
|
||||
|
||||
function normalizeScopeInput(value = SCOPE_AUTO) {
|
||||
const normalized = String(value || SCOPE_AUTO).trim().toLowerCase();
|
||||
if (!normalized || normalized === SCOPE_AUTO) return SCOPE_AUTO;
|
||||
@@ -4292,176 +4127,6 @@ function buildQueryModeSql(args) {
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeMatchRole(value) {
|
||||
const normalized = String(value || "team").trim().toLowerCase();
|
||||
if (["team", "member", "resource", "participant"].includes(normalized)) {
|
||||
return "team";
|
||||
}
|
||||
if (["lead", "leader", "owner"].includes(normalized)) {
|
||||
return "lead";
|
||||
}
|
||||
throw new Error(`Unsupported match role "${value}". Use team or lead.`);
|
||||
}
|
||||
|
||||
function normalizeOracleEmailInput(value, missingMessage) {
|
||||
const raw = String(value || "").trim().toLowerCase();
|
||||
const email = raw && raw.includes("@") ? raw : raw ? `${raw}@oracle.com` : "";
|
||||
if (!email) {
|
||||
throw new Error(missingMessage);
|
||||
}
|
||||
return {
|
||||
rawInput: raw,
|
||||
email,
|
||||
emailSql: quoteLiteral(email),
|
||||
};
|
||||
}
|
||||
|
||||
function resolveResourceFilter(args) {
|
||||
const { rawInput, email, emailSql } = normalizeOracleEmailInput(
|
||||
args["resource-email"] || args.resource || "",
|
||||
"Pass --resource-email or --resource when using a resource-scoped query."
|
||||
);
|
||||
const matchRole = normalizeMatchRole(args["match-role"] || "team");
|
||||
return {
|
||||
resourceInput: rawInput,
|
||||
resourceEmail: email,
|
||||
resourceEmailSql: emailSql,
|
||||
matchRole,
|
||||
srStatuses: splitCsv(args["sr-status"], []),
|
||||
};
|
||||
}
|
||||
|
||||
function resolveManagerEmailFilter(args) {
|
||||
const { rawInput, email, emailSql } = normalizeOracleEmailInput(
|
||||
args["manager-email"] || "",
|
||||
"Pass --manager-email when using a manager-scoped opportunity query."
|
||||
);
|
||||
return {
|
||||
managerInput: rawInput,
|
||||
managerEmail: email,
|
||||
managerEmailSql: emailSql,
|
||||
};
|
||||
}
|
||||
|
||||
function resolveManagerEmailListFilter(args) {
|
||||
const tokens = [
|
||||
...splitCsv(args["manager-email-list"], []),
|
||||
...splitCsv(args["manager-email"], []),
|
||||
];
|
||||
const normalizedEmails = Array.from(
|
||||
new Set(
|
||||
tokens
|
||||
.map((token) =>
|
||||
normalizeOracleEmailInput(
|
||||
token,
|
||||
"Pass --manager-email-list or --manager-email when using a manager/resource workload query."
|
||||
).email
|
||||
)
|
||||
.filter(Boolean)
|
||||
)
|
||||
).sort();
|
||||
if (normalizedEmails.length === 0) {
|
||||
throw new Error("Pass --manager-email-list or --manager-email when using a manager/resource workload query.");
|
||||
}
|
||||
return {
|
||||
managerEmails: normalizedEmails,
|
||||
managerEmailsSqlList: toSqlList(normalizedEmails),
|
||||
};
|
||||
}
|
||||
|
||||
function resolveOwnerEmailFilter(args, flagName = "owner-email", missingMessage = "Pass --owner-email when using an owner-scoped opportunity query.") {
|
||||
const { rawInput, email, emailSql } = normalizeOracleEmailInput(
|
||||
args[flagName] || "",
|
||||
missingMessage
|
||||
);
|
||||
return {
|
||||
ownerInput: rawInput,
|
||||
ownerEmail: email,
|
||||
ownerEmailSql: emailSql,
|
||||
};
|
||||
}
|
||||
|
||||
function applyCurrentWorkloadIntentDefaults(args) {
|
||||
const nextArgs = { ...args };
|
||||
if (!hasAnyDateWindowArgs(nextArgs)) {
|
||||
nextArgs.quarter = "current-quarter";
|
||||
}
|
||||
if (!nextArgs.solution) {
|
||||
nextArgs.solution = "Cloud Infrastructure - Workloads";
|
||||
}
|
||||
if (!nextArgs["revenue-type-group"]) {
|
||||
nextArgs["revenue-type-group"] = "WORKLOAD";
|
||||
}
|
||||
return nextArgs;
|
||||
}
|
||||
|
||||
function applyCloseWindowIntentDefaults(args) {
|
||||
const nextArgs = { ...args };
|
||||
if (!hasAnyDateWindowArgs(nextArgs)) {
|
||||
nextArgs["from-date"] = "today";
|
||||
nextArgs["to-date"] = "+7d";
|
||||
}
|
||||
return nextArgs;
|
||||
}
|
||||
|
||||
function resolveSaAttachFilters(args, definition) {
|
||||
const statuses = splitCsv(args["revenue-line-status"], definition.defaultStatuses || SA_ATTACH_DEFAULT_ALL_STATUSES);
|
||||
const cluster = String(args.cluster || SA_ATTACH_DEFAULT_CLUSTER).trim();
|
||||
const solutions = splitCsv(args.solution, SA_ATTACH_DEFAULT_SOLUTIONS);
|
||||
const productLobs = normalizeSaAttachProductLobs(
|
||||
splitCsv(args["executive-product-lob"], SA_ATTACH_DEFAULT_PRODUCT_LOBS)
|
||||
);
|
||||
const revenueTypeGroups = normalizeSaAttachRevenueTypeGroups(
|
||||
splitCsv(args["revenue-type-group"], SA_ATTACH_DEFAULT_REVENUE_TYPE_GROUPS)
|
||||
);
|
||||
const salesCreditType = String(args["sales-credit-type"] || SA_ATTACH_DEFAULT_SALES_CREDIT_TYPE).trim();
|
||||
const minOpportunityProbability = Number(
|
||||
args["min-opportunity-probability"] || SA_ATTACH_DEFAULT_MIN_OPPORTUNITY_PROBABILITY
|
||||
);
|
||||
const ownerEmail = args["owner-email"]
|
||||
? normalizeOracleEmailInput(args["owner-email"], "Pass --owner-email with a valid Oracle email.").email
|
||||
: null;
|
||||
const ownerManagerEmail = args["owner-manager-email"]
|
||||
? normalizeOracleEmailInput(args["owner-manager-email"], "Pass --owner-manager-email with a valid Oracle email.").email
|
||||
: null;
|
||||
const forecastTypes = splitCsv(args["forecast-type"], []);
|
||||
|
||||
if (!cluster) {
|
||||
throw new Error("Cluster cannot be empty for SA Attach views.");
|
||||
}
|
||||
if (solutions.length === 0) {
|
||||
throw new Error("Pass at least one --solution value for SA Attach views.");
|
||||
}
|
||||
if (statuses.length === 0) {
|
||||
throw new Error("Pass at least one --revenue-line-status value for SA Attach views.");
|
||||
}
|
||||
if (productLobs.length === 0) {
|
||||
throw new Error("Pass at least one --executive-product-lob value for SA Attach views.");
|
||||
}
|
||||
if (revenueTypeGroups.length === 0) {
|
||||
throw new Error("Pass at least one --revenue-type-group value for SA Attach views.");
|
||||
}
|
||||
if (!salesCreditType) {
|
||||
throw new Error("Sales credit type cannot be empty for SA Attach views.");
|
||||
}
|
||||
if (!Number.isFinite(minOpportunityProbability)) {
|
||||
throw new Error("Pass a numeric --min-opportunity-probability value for SA Attach views.");
|
||||
}
|
||||
|
||||
return {
|
||||
cluster,
|
||||
solutions,
|
||||
statuses,
|
||||
productLobs,
|
||||
revenueTypeGroups,
|
||||
salesCreditType,
|
||||
minOpportunityProbability,
|
||||
ownerEmail,
|
||||
ownerManagerEmail,
|
||||
forecastTypes,
|
||||
};
|
||||
}
|
||||
|
||||
function buildSrResourcePredicate(resourceEmailSql, matchRole) {
|
||||
if (matchRole === "lead") {
|
||||
return `"Service Request"."Lead Email" = ${resourceEmailSql}`;
|
||||
@@ -4604,19 +4269,6 @@ function buildManagerResourceCoverageSqlForOpportunityIds(opportunityIds, manage
|
||||
});
|
||||
}
|
||||
|
||||
function resolveOptionalClusterFilter(args) {
|
||||
const cluster = typeof args.cluster === "string" ? args.cluster.trim() : "";
|
||||
return cluster || null;
|
||||
}
|
||||
|
||||
function resolveCommercialClusterFilter(args, fallback = SA_ATTACH_DEFAULT_CLUSTER) {
|
||||
return resolveOptionalClusterFilter(args) || fallback;
|
||||
}
|
||||
|
||||
function resolveCoverageLimit(limit) {
|
||||
return Math.min(10000, Math.max(5000, Number(limit) * 8));
|
||||
}
|
||||
|
||||
function buildWorkloadCloseWindowSql({
|
||||
fromDateSql,
|
||||
toDateSql,
|
||||
@@ -5325,286 +4977,6 @@ function buildIntentScopeDescriptor(name, definition) {
|
||||
return buildViewScopeDescriptor(definition.view, VIEW_DEFS[definition.view] || {});
|
||||
}
|
||||
|
||||
function hasUsableOpportunityId(value) {
|
||||
const normalized = String(value || "").trim();
|
||||
return Boolean(normalized && normalized.toLowerCase() !== "unspecified");
|
||||
}
|
||||
|
||||
function buildCustomerMatchKey(value) {
|
||||
return String(value || "")
|
||||
.normalize("NFKD")
|
||||
.replace(/[\u0300-\u036f]/g, "")
|
||||
.replace(/\s+/g, " ")
|
||||
.trim()
|
||||
.toLowerCase();
|
||||
}
|
||||
|
||||
function normalizeEmailAddress(value) {
|
||||
return String(value || "").trim().toLowerCase();
|
||||
}
|
||||
|
||||
function toSortedUniqueStrings(values) {
|
||||
return Array.from(
|
||||
new Set(
|
||||
(values || [])
|
||||
.map((value) => String(value || "").trim())
|
||||
.filter(Boolean)
|
||||
)
|
||||
).sort((left, right) => left.localeCompare(right));
|
||||
}
|
||||
|
||||
function sortCandidateInternalSrDetails(details) {
|
||||
return Array.from(
|
||||
new Map(
|
||||
(details || [])
|
||||
.filter((detail) => detail?.srNumber)
|
||||
.map((detail) => [String(detail.srNumber || "").trim(), detail])
|
||||
).values()
|
||||
).sort((left, right) => String(left.srNumber || "").localeCompare(String(right.srNumber || "")));
|
||||
}
|
||||
|
||||
function buildSrLinkageMode(directSrNumbers, candidateInternalSrDetails) {
|
||||
const directCount = toSortedUniqueStrings(directSrNumbers).length;
|
||||
const candidateCount = sortCandidateInternalSrDetails(candidateInternalSrDetails).length;
|
||||
if (directCount > 0 && candidateCount > 0) return "direct-plus-candidate-internal";
|
||||
if (directCount > 0) return "direct-only";
|
||||
if (candidateCount > 0) return "candidate-internal-only";
|
||||
return "none";
|
||||
}
|
||||
|
||||
function buildResolvedSrNumbers(directSrNumbers, candidateInternalSrDetails) {
|
||||
return toSortedUniqueStrings([
|
||||
...toSortedUniqueStrings(directSrNumbers),
|
||||
...sortCandidateInternalSrDetails(candidateInternalSrDetails).map((detail) => detail.srNumber),
|
||||
]);
|
||||
}
|
||||
|
||||
function buildInternalSrAwareResourceDetails({
|
||||
opportunityId,
|
||||
customerName,
|
||||
resourceEmails,
|
||||
srNumbersByResourceOpportunity = new Map(),
|
||||
candidateInternalSrDetailsByResource = new Map(),
|
||||
}) {
|
||||
const customerMatchKey = buildCustomerMatchKey(customerName);
|
||||
const normalizedResourceEmails = toSortedUniqueStrings(
|
||||
(resourceEmails || []).map((email) => normalizeEmailAddress(email))
|
||||
);
|
||||
return normalizedResourceEmails.map((resourceEmail) => {
|
||||
const directSrNumbers = toSortedUniqueStrings(
|
||||
Array.from(srNumbersByResourceOpportunity.get(buildSrCoverageKey(opportunityId, resourceEmail)) || [])
|
||||
);
|
||||
const candidateInternalSrDetails = sortCandidateInternalSrDetails(
|
||||
Array.from(candidateInternalSrDetailsByResource.get(resourceEmail)?.values() || []).filter(
|
||||
(candidate) => buildCustomerMatchKey(candidate.customerName) === customerMatchKey
|
||||
)
|
||||
);
|
||||
return {
|
||||
resourceEmail,
|
||||
srNumbers: buildResolvedSrNumbers(directSrNumbers, candidateInternalSrDetails),
|
||||
directSrNumbers,
|
||||
candidateInternalSrNumbers: candidateInternalSrDetails.map((candidate) => candidate.srNumber),
|
||||
candidateInternalSrDetails,
|
||||
srLinkageMode: buildSrLinkageMode(directSrNumbers, candidateInternalSrDetails),
|
||||
companionEmails: normalizedResourceEmails.filter((email) => email !== resourceEmail),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function summarizeCurrentTeamOpportunityCoverage(opportunities, coverageRows) {
|
||||
const opportunityMap = new Map(
|
||||
opportunities
|
||||
.filter((record) => record?.opportunityId)
|
||||
.map((record) => [
|
||||
record.opportunityId,
|
||||
{
|
||||
...record,
|
||||
bookingValue: parseNumericValue(record.bookingValue),
|
||||
workloadAmount: parseNumericValue(record.workloadAmount),
|
||||
},
|
||||
])
|
||||
);
|
||||
|
||||
const coverageByOpportunity = new Map();
|
||||
for (const row of coverageRows) {
|
||||
const opportunityId = row?.opportunityId;
|
||||
if (!opportunityId || !opportunityMap.has(opportunityId)) continue;
|
||||
const entry = coverageByOpportunity.get(opportunityId) || {
|
||||
opportunityId,
|
||||
teamMemberEmails: new Set(),
|
||||
currentTeamOpportunityHours: 0,
|
||||
coverageRows: 0,
|
||||
};
|
||||
for (const fieldName of TEAM_EMAIL_COLUMN_NAMES) {
|
||||
const email = row?.[fieldName];
|
||||
if (email) entry.teamMemberEmails.add(email);
|
||||
}
|
||||
entry.currentTeamOpportunityHours += parseNumericValue(
|
||||
row?.opportunityHoursAttribute ?? row?.opportunityHours
|
||||
);
|
||||
entry.coverageRows += 1;
|
||||
coverageByOpportunity.set(opportunityId, entry);
|
||||
}
|
||||
|
||||
const totalWindowOpportunities = opportunityMap.size;
|
||||
const records = Array.from(opportunityMap.values())
|
||||
.map((opportunity) => {
|
||||
const coverage = coverageByOpportunity.get(opportunity.opportunityId) || {
|
||||
opportunityId: opportunity.opportunityId,
|
||||
teamMemberEmails: new Set(),
|
||||
currentTeamOpportunityHours: 0,
|
||||
coverageRows: 0,
|
||||
};
|
||||
return {
|
||||
opportunityId: opportunity.opportunityId,
|
||||
opportunityName: opportunity.opportunityName,
|
||||
customerName: opportunity.customerName,
|
||||
opportunityStatus: opportunity.opportunityStatus,
|
||||
closeDate: opportunity.closeDate,
|
||||
salesStage: opportunity.salesStage,
|
||||
bookingValue: opportunity.bookingValue,
|
||||
workloadAmount: opportunity.workloadAmount,
|
||||
teamMemberEmails: Array.from(coverage.teamMemberEmails).sort(),
|
||||
currentTeamOpportunityHours: coverage.currentTeamOpportunityHours,
|
||||
coverageRows: coverage.coverageRows,
|
||||
};
|
||||
})
|
||||
.sort((left, right) => {
|
||||
const closeDateCompare = String(left.closeDate || "").localeCompare(String(right.closeDate || ""));
|
||||
if (closeDateCompare !== 0) return closeDateCompare;
|
||||
return String(left.opportunityId || "").localeCompare(String(right.opportunityId || ""));
|
||||
});
|
||||
|
||||
const summary = records.reduce(
|
||||
(totals, record) => {
|
||||
const matched = parseNumericValue(record.coverageRows) > 0;
|
||||
return {
|
||||
totalWindowOpportunities,
|
||||
matchedOpportunities: totals.matchedOpportunities + (matched ? 1 : 0),
|
||||
unmatchedOpportunities: totals.unmatchedOpportunities + (matched ? 0 : 1),
|
||||
totalBookingValue: totals.totalBookingValue + parseNumericValue(record.bookingValue),
|
||||
totalWorkloadAmount: totals.totalWorkloadAmount + parseNumericValue(record.workloadAmount),
|
||||
matchedBookingValue:
|
||||
totals.matchedBookingValue + (matched ? parseNumericValue(record.bookingValue) : 0),
|
||||
matchedWorkloadAmount:
|
||||
totals.matchedWorkloadAmount + (matched ? parseNumericValue(record.workloadAmount) : 0),
|
||||
totalCurrentTeamOpportunityHours:
|
||||
totals.totalCurrentTeamOpportunityHours + parseNumericValue(record.currentTeamOpportunityHours),
|
||||
};
|
||||
},
|
||||
{
|
||||
totalWindowOpportunities,
|
||||
matchedOpportunities: 0,
|
||||
unmatchedOpportunities: 0,
|
||||
totalBookingValue: 0,
|
||||
totalWorkloadAmount: 0,
|
||||
matchedBookingValue: 0,
|
||||
matchedWorkloadAmount: 0,
|
||||
totalCurrentTeamOpportunityHours: 0,
|
||||
}
|
||||
);
|
||||
|
||||
return { records, summary };
|
||||
}
|
||||
|
||||
function summarizeOrgWideRevenueLineCoverage(
|
||||
workloadRows,
|
||||
coverageRows,
|
||||
srNumbersByResourceOpportunity = new Map(),
|
||||
candidateInternalSrDetailsByResource = new Map()
|
||||
) {
|
||||
const includeSrDetails =
|
||||
srNumbersByResourceOpportunity.size > 0 || candidateInternalSrDetailsByResource.size > 0;
|
||||
const workloadRecords = workloadRows
|
||||
.filter((record) => record?.revenueLineId && record?.opportunityId)
|
||||
.map((record) => ({
|
||||
closeDate: record.closeDate,
|
||||
revenueLineId: record.revenueLineId,
|
||||
opportunityId: record.opportunityId,
|
||||
opportunityName: record.opportunityName,
|
||||
customerName: record.customerName,
|
||||
workloadAmount: parseNumericValue(record.workloadAmount),
|
||||
}));
|
||||
|
||||
const matchedOpportunities = new Map();
|
||||
for (const row of coverageRows) {
|
||||
const opportunityId = row?.opportunityId;
|
||||
const managerEmail = row?.seTeamLevel4EmailAddress;
|
||||
const resourceEmail = row?.seTeamLevel5EmailAddress;
|
||||
if (!opportunityId || !managerEmail || !resourceEmail) continue;
|
||||
const entry = matchedOpportunities.get(opportunityId) || {
|
||||
managerEmails: new Set(),
|
||||
resourceEmails: new Set(),
|
||||
};
|
||||
entry.managerEmails.add(normalizeEmailAddress(managerEmail));
|
||||
entry.resourceEmails.add(normalizeEmailAddress(resourceEmail));
|
||||
matchedOpportunities.set(opportunityId, entry);
|
||||
}
|
||||
|
||||
const matchedRecords = workloadRecords
|
||||
.filter((record) => matchedOpportunities.has(record.opportunityId))
|
||||
.map((record) => {
|
||||
const coverage = matchedOpportunities.get(record.opportunityId);
|
||||
const resourceEmails = toSortedUniqueStrings(Array.from(coverage.resourceEmails));
|
||||
const resourceDetails = includeSrDetails
|
||||
? buildInternalSrAwareResourceDetails({
|
||||
opportunityId: record.opportunityId,
|
||||
customerName: record.customerName,
|
||||
resourceEmails,
|
||||
srNumbersByResourceOpportunity,
|
||||
candidateInternalSrDetailsByResource,
|
||||
})
|
||||
: resourceEmails.map((resourceEmail) => ({
|
||||
resourceEmail,
|
||||
companionEmails: resourceEmails.filter((email) => email !== resourceEmail),
|
||||
}));
|
||||
return {
|
||||
...record,
|
||||
managerEmails: toSortedUniqueStrings(Array.from(coverage.managerEmails)),
|
||||
resourceEmails,
|
||||
resourceDetails,
|
||||
};
|
||||
})
|
||||
.sort((left, right) => {
|
||||
const closeDateCompare = String(left.closeDate || "").localeCompare(String(right.closeDate || ""));
|
||||
if (closeDateCompare !== 0) return closeDateCompare;
|
||||
const opportunityCompare = String(left.opportunityId || "").localeCompare(String(right.opportunityId || ""));
|
||||
if (opportunityCompare !== 0) return opportunityCompare;
|
||||
return String(left.revenueLineId || "").localeCompare(String(right.revenueLineId || ""));
|
||||
});
|
||||
|
||||
const distinctMatchedOpportunities = new Set(matchedRecords.map((record) => record.opportunityId));
|
||||
const distinctMatchedRevenueLines = new Set(matchedRecords.map((record) => record.revenueLineId));
|
||||
const summary = matchedRecords.reduce(
|
||||
(totals, record) => ({
|
||||
totalWindowRevenueLines: totals.totalWindowRevenueLines,
|
||||
matchedRevenueLines: totals.matchedRevenueLines + 1,
|
||||
matchedOpportunities: totals.matchedOpportunities,
|
||||
totalWorkloadAmount: totals.totalWorkloadAmount + parseNumericValue(record.workloadAmount),
|
||||
managersRequested: totals.managersRequested,
|
||||
resourcesMatched: totals.resourcesMatched,
|
||||
}),
|
||||
{
|
||||
totalWindowRevenueLines: workloadRecords.length,
|
||||
matchedRevenueLines: 0,
|
||||
matchedOpportunities: distinctMatchedOpportunities.size,
|
||||
totalWorkloadAmount: 0,
|
||||
managersRequested: 0,
|
||||
resourcesMatched: Array.from(
|
||||
new Set(
|
||||
matchedRecords.flatMap((record) => record.resourceEmails || [])
|
||||
)
|
||||
).length,
|
||||
}
|
||||
);
|
||||
summary.matchedRevenueLines = distinctMatchedRevenueLines.size;
|
||||
return { records: matchedRecords, summary };
|
||||
}
|
||||
|
||||
function buildSrCoverageKey(opportunityId, resourceEmail) {
|
||||
return `${String(opportunityId || "").trim()}::${String(resourceEmail || "").trim().toLowerCase()}`;
|
||||
}
|
||||
|
||||
async function fetchResourceOpportunitySrCoverage({
|
||||
resourceEmails,
|
||||
opportunityIds,
|
||||
|
||||
Reference in New Issue
Block a user