import type { ApiDiscoveredProfile, ApiEnrichedContactData, ApiEnrichmentSuggestion, } from './discovery-types' import type { FullContact } from './types' const LABEL_MAP: Record = { work: 'Travail', home: 'Domicile', mobile: 'Mobile', other: 'Autre', linkedin: 'LinkedIn', twitter: 'X / Twitter', facebook: 'Facebook', instagram: 'Instagram', github: 'GitHub', } function mapLabel(label?: string): string { if (!label) return 'Autre' return LABEL_MAP[label.toLowerCase()] ?? label } export function discoveredProfileToFullContact(profile: ApiDiscoveredProfile): FullContact { const data: ApiEnrichedContactData | undefined = profile.enriched_data const now = Date.now() const emails = data?.emails?.length ? data.emails.map((e) => ({ value: e.value, label: mapLabel(e.label) })) : [{ value: profile.primary_email, label: 'Autre' }] const phones = (data?.phones ?? []).map((p) => ({ value: p.value, label: mapLabel(p.label), })) const addresses = (data?.addresses ?? []).map((a) => ({ street: a.street, city: a.city, region: a.region, postalCode: a.postal_code, country: a.country, label: mapLabel(a.label), })) const socialProfiles = (data?.social_profiles ?? []).map((p) => ({ value: p.value, label: mapLabel(p.label), })) return { id: profile.id, firstName: data?.first_name ?? profile.display_name.split(' ')[0] ?? '', lastName: data?.last_name ?? profile.display_name.split(' ').slice(1).join(' ') ?? '', company: data?.company, department: data?.department, jobTitle: data?.job_title, website: data?.website, socialProfiles: socialProfiles.length > 0 ? socialProfiles : undefined, emails, phones, addresses: addresses.length > 0 ? addresses : undefined, notes: data?.notes, interactionCount: profile.message_count, isOtherContact: true, createdAt: now, updatedAt: now, } } function emailLocalPart(email: string): string { const at = email.lastIndexOf('@') if (at <= 0) return email.trim().toLowerCase() return email.slice(0, at).trim().toLowerCase() } function hasMeaningfulDisplayName(name: string | undefined, email: string): boolean { const trimmed = name?.trim() if (!trimmed || trimmed.includes('@')) return false const local = emailLocalPart(email) if (!local) return true return trimmed.toLowerCase() !== local } function enrichedDataHasValueBeyondEmail( data: ApiEnrichedContactData | undefined | null, ): boolean { if (!data) return false if ( data.first_name?.trim() || data.last_name?.trim() || data.company?.trim() || data.department?.trim() || data.job_title?.trim() || data.website?.trim() || data.notes?.trim() ) { return true } if (data.social_profiles?.some((p) => p.value?.trim())) return true if (data.phones?.some((p) => p.value?.trim())) return true return ( data.addresses?.some( (a) => a.street?.trim() || a.city?.trim() || a.region?.trim() || a.postal_code?.trim() || a.country?.trim(), ) ?? false ) } /** True when profile has more than a bare email (signature, name, enriched fields, etc.). */ export function profileHasValueBeyondEmail(profile: ApiDiscoveredProfile): boolean { if ((profile.signatures?.length ?? 0) > 0) return true if (enrichedDataHasValueBeyondEmail(profile.enriched_data)) return true if (hasMeaningfulDisplayName(profile.display_name, profile.primary_email)) return true for (const e of profile.all_emails ?? []) { if (hasMeaningfulDisplayName(e.display_name, e.email)) return true } return false } export function isSuggestableDiscoveryGroup(group: { profile?: ApiDiscoveredProfile profiles?: ApiDiscoveredProfile[] }): boolean { const profiles = group.profiles?.length ? group.profiles : group.profile ? [group.profile] : [] if (profiles.some((p) => profileHasNoReplyEmail(p))) return false return profiles.some((p) => profileHasValueBeyondEmail(p)) } export function profileDisplayName(profile: ApiDiscoveredProfile): string { const data = profile.enriched_data if (data?.first_name || data?.last_name) { return `${data.first_name ?? ''} ${data.last_name ?? ''}`.trim() } return profile.display_name || profile.primary_email } function normalizePhone(s: string): string { return s.replace(/\D/g, '') } function phonesMatch(a: string, b: string): boolean { const na = normalizePhone(a) const nb = normalizePhone(b) if (!na || !nb) return false if (na.length >= 9 && nb.length >= 9) { return na.slice(-9) === nb.slice(-9) } return na === nb } function stringsEqualInsensitive(a: string, b: string): boolean { return a.trim().toLowerCase() === b.trim().toLowerCase() } export function isNoReplyEmail(email: string): boolean { const lower = email.trim().toLowerCase() return ( lower.includes('noreply') || lower.includes('no-reply') || lower.includes('no_reply') ) } function profileHasNoReplyEmail(profile: ApiDiscoveredProfile): boolean { if (isNoReplyEmail(profile.primary_email)) return true for (const e of profile.all_emails ?? []) { if (isNoReplyEmail(e.email)) return true } for (const e of profile.enriched_data?.emails ?? []) { if (isNoReplyEmail(e.value)) return true } return false } /** Suggestions shown in « Ajouter des coordonnées » (enrichissement de contacts existants). */ export function filterVisibleEnrichmentSuggestions( rawSuggestions: ApiEnrichmentSuggestion[], contacts: FullContact[], ): ApiEnrichmentSuggestion[] { return rawSuggestions.filter((s) => { if (s.suggestion_type !== 'enrich_contact' || !s.target_contact_uid) { return true } const contact = contacts.find((c) => c.id === s.target_contact_uid) if (!contact) return true return !isEnrichmentSuggestionRedundant(contact, s) }) } /** True when the suggestion value is already present on the contact. */ export function isEnrichmentSuggestionRedundant( contact: FullContact, suggestion: Pick, ): boolean { const val = suggestion.suggested_value.trim() if (!val) return true switch (suggestion.field_path) { case 'full_name': { const full = `${contact.firstName} ${contact.lastName}`.trim() return stringsEqualInsensitive(val, full) } case 'company': return contact.company ? stringsEqualInsensitive(val, contact.company) : false case 'job_title': return contact.jobTitle ? stringsEqualInsensitive(val, contact.jobTitle) : false case 'phones': return contact.phones.some((p) => phonesMatch(p.value, val)) case 'emails': return contact.emails.some((e) => stringsEqualInsensitive(e.value, val)) case 'social_profiles': return (contact.socialProfiles ?? []).some((p) => stringsEqualInsensitive(p.value, val)) default: return false } } export const FIELD_LABELS: Record = { first_name: 'Prénom', last_name: 'Nom', company: 'Entreprise', department: 'Service', job_title: 'Poste', emails: 'E-mail', phones: 'Téléphone', addresses: 'Adresse', website: 'Site web', social_profiles: 'Réseaux sociaux', notes: 'Notes', full_name: 'Nom complet', }