/** * Merge one generated sequence row (email or LinkedIn) into grouped contacts for previews. * When stepOrder is present (campaign wizard), dedupe by stepOrder + channel so merged timelines stay stable. * Always returns new references (immutable) so React re-renders when LinkedIn rows stream in after emails. */ export function mergeSequenceIntoContacts(prev, sequence) { const ch = sequence.channel || 'email'; const matchIdx = prev.findIndex( (c) => c.firstName === sequence.firstName && c.lastName === sequence.lastName && c.email === sequence.email ); const stepOrder = sequence.stepOrder != null && sequence.stepOrder !== undefined ? sequence.stepOrder : null; const step = { emailNumber: sequence.emailNumber || 1, subject: sequence.subject, emailContent: sequence.emailContent, channel: ch, stepOrder, }; const dedupeKey = stepOrder != null ? `ord-${stepOrder}-${ch}` : `${ch}-${step.emailNumber}`; if (matchIdx === -1) { const base = { id: sequence.id, firstName: sequence.firstName, lastName: sequence.lastName, email: sequence.email, company: sequence.company, title: sequence.title, product: sequence.product, emails: [], linkedin: [], }; if (ch === 'linkedin') { base.linkedin = [step]; } else { base.emails = [step]; } return [...prev, base]; } const existing = prev[matchIdx]; const emails = [...(existing.emails || [])]; const linkedin = [...(existing.linkedin || [])]; if (ch === 'linkedin') { if ( !linkedin.some((e) => { const k = e.stepOrder != null ? `ord-${e.stepOrder}-${ch}` : `${ch}-${e.emailNumber}`; return k === dedupeKey; }) ) { linkedin.push(step); } } else { if ( !emails.some((e) => { const k = e.stepOrder != null ? `ord-${e.stepOrder}-${ch}` : `${ch}-${e.emailNumber}`; return k === dedupeKey; }) ) { emails.push(step); } } const updated = { ...existing, emails, linkedin, }; return prev.map((c, i) => (i === matchIdx ? updated : c)); }