import { parseNumericValue } from "./output-contract.mjs"; const TEAM_EMAIL_COLUMN_NAMES = Array.from({ length: 8 }, (_, index) => `seTeamLevel${index + 1}EmailAddress`); // Objetivo: validar se um opportunityId pode ser usado em joins. Entrada: valor bruto. Saida: booleano. export function hasUsableOpportunityId(value) { const normalized = String(value || "").trim(); return Boolean(normalized && normalized.toLowerCase() !== "unspecified"); } // Objetivo: normalizar nome de cliente para comparacao local. Entrada: nome bruto. Saida: chave sem acentos e em minusculas. function buildCustomerMatchKey(value) { return String(value || "") .normalize("NFKD") .replace(/[\u0300-\u036f]/g, "") .replace(/\s+/g, " ") .trim() .toLowerCase(); } // Objetivo: normalizar email para comparacao. Entrada: valor bruto. Saida: email em minusculas. function normalizeEmailAddress(value) { return String(value || "").trim().toLowerCase(); } // Objetivo: remover vazios/duplicados e ordenar textos. Entrada: lista de valores. Saida: strings unicas ordenadas. function toSortedUniqueStrings(values) { return Array.from( new Set((values || []).map((value) => String(value || "").trim()).filter(Boolean)) ).sort((left, right) => left.localeCompare(right)); } // Objetivo: deduplicar SRs internos candidatos por numero. Entrada: detalhes de SR. Saida: detalhes ordenados por SR. 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 || ""))); } // Objetivo: classificar como SRs foram ligados ao recurso. Entrada: SRs diretos e candidatos internos. Saida: modo textual. 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"; } // Objetivo: unir SRs diretos e candidatos. Entrada: listas de SR. Saida: SRs unicos ordenados. function buildResolvedSrNumbers(directSrNumbers, candidateInternalSrDetails) { return toSortedUniqueStrings([ ...toSortedUniqueStrings(directSrNumbers), ...sortCandidateInternalSrDetails(candidateInternalSrDetails).map((detail) => detail.srNumber), ]); } // Objetivo: construir chave de coverage por oportunidade e recurso. Entrada: opportunityId e email. Saida: chave normalizada. export function buildSrCoverageKey(opportunityId, resourceEmail) { return `${String(opportunityId || "").trim()}::${normalizeEmailAddress(resourceEmail)}`; } // Objetivo: montar detalhes de recursos com SRs diretos e candidatos internos. Entrada: oportunidade, recursos e mapas de SR. Saida: detalhes por recurso. function buildInternalSrAwareResourceDetails({ opportunityId, customerName, resourceEmails, srNumbersByResourceOpportunity = new Map(), candidateInternalSrDetailsByResource = new Map(), }) { const customerMatchKey = buildCustomerMatchKey(customerName); const normalizedResourceEmails = toSortedUniqueStrings((resourceEmails || []).map(normalizeEmailAddress)); 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), }; }); } // Objetivo: sumarizar oportunidades comerciais e coverage do time atual preservando nao casadas. Entrada: oportunidades e linhas coverage. Saida: records e summary. export 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 || "")); return closeDateCompare || 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 }; } // Objetivo: sumarizar workloads org-wide cruzados com coverage de recursos. Entrada: workloads, coverage e SRs opcionais. Saida: records e summary. export 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 || "")); return opportunityCompare || 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 = { totalWindowRevenueLines: workloadRecords.length, matchedRevenueLines: distinctMatchedRevenueLines.size, matchedOpportunities: distinctMatchedOpportunities.size, totalWorkloadAmount: matchedRecords.reduce( (total, record) => total + parseNumericValue(record.workloadAmount), 0 ), managersRequested: 0, resourcesMatched: new Set(matchedRecords.flatMap((record) => record.resourceEmails || [])).size, }; return { records: matchedRecords, summary }; }