Some checks are pending
E2E / Playwright e2e (push) Waiting to run
- Introduced new ContactAvatar and ContactAvatarPicker components for enhanced avatar management in contact views. - Updated ContactDetailView and ContactFormView to utilize the new avatar components, improving user experience when adding or editing contacts. - Enhanced ContactHoverCard and ContactRow components to display avatars, providing a more visually appealing interface. - Added loading and error states in ContactsListView for better user feedback during data fetching. - Implemented a new ContactsLoadState component to handle loading and error scenarios in the contacts list. - Updated package.json to include @formkit/auto-animate for improved UI animations.
225 lines
6.4 KiB
TypeScript
225 lines
6.4 KiB
TypeScript
import type { ApiDiscoveredProfile, ApiDiscoveredProfileGroup } from './discovery-types'
|
|
import type { FullContact } from './types'
|
|
import { FIELD_LABELS, discoveredProfileToFullContact } from './discovery-utils'
|
|
import type { ContactAddress } from './types'
|
|
|
|
export interface DraftField {
|
|
id: string
|
|
label: string
|
|
fieldKey: string
|
|
value: string
|
|
removed: boolean
|
|
}
|
|
|
|
function fieldId(fieldKey: string, value: string) {
|
|
return `${fieldKey}::${value}`
|
|
}
|
|
|
|
/** Clé de déduplication par valeur normalisée (indépendante du libellé / source). */
|
|
function normalizedValueKey(fieldKey: string, value: string): string {
|
|
const v = value.trim()
|
|
if (fieldKey === 'emails') {
|
|
return `emails:${v.toLowerCase()}`
|
|
}
|
|
if (fieldKey === 'phones') {
|
|
const digits = v.replace(/\D/g, '')
|
|
return digits.length >= 6 ? `phones:${digits}` : `phones:${v.toLowerCase()}`
|
|
}
|
|
if (fieldKey === 'addresses') {
|
|
return `addresses:${v.toLowerCase()}`
|
|
}
|
|
return `${fieldKey}:${v.toLowerCase()}`
|
|
}
|
|
|
|
function pushDraftField(
|
|
out: DraftField[],
|
|
seen: Set<string>,
|
|
fieldKey: string,
|
|
value: string,
|
|
label: string,
|
|
) {
|
|
const v = value.trim()
|
|
if (!v) return
|
|
const dedupeKey = normalizedValueKey(fieldKey, v)
|
|
if (seen.has(dedupeKey)) return
|
|
seen.add(dedupeKey)
|
|
out.push({
|
|
id: fieldId(fieldKey, fieldKey === 'emails' ? v.toLowerCase() : v),
|
|
label,
|
|
fieldKey,
|
|
value: v,
|
|
removed: false,
|
|
})
|
|
}
|
|
|
|
function pushScalar(
|
|
out: DraftField[],
|
|
fieldKey: string,
|
|
value: string | undefined,
|
|
seen: Set<string>,
|
|
) {
|
|
const v = value?.trim()
|
|
if (!v) return
|
|
pushDraftField(out, seen, fieldKey, v, FIELD_LABELS[fieldKey] ?? fieldKey)
|
|
}
|
|
|
|
function pushLabeled(
|
|
out: DraftField[],
|
|
fieldKey: string,
|
|
items: { value: string; label?: string }[] | undefined,
|
|
seen: Set<string>,
|
|
) {
|
|
for (const item of items ?? []) {
|
|
const v = item.value?.trim()
|
|
if (!v) continue
|
|
const suffix = item.label ? ` (${item.label})` : ''
|
|
pushDraftField(
|
|
out,
|
|
seen,
|
|
fieldKey,
|
|
v,
|
|
`${FIELD_LABELS[fieldKey] ?? fieldKey}${suffix}`,
|
|
)
|
|
}
|
|
}
|
|
|
|
export function buildDraftFields(group: ApiDiscoveredProfileGroup): DraftField[] {
|
|
const profile = group.profile ?? group.profiles?.[0]
|
|
if (!profile) return []
|
|
|
|
const data = profile.enriched_data
|
|
const seen = new Set<string>()
|
|
const out: DraftField[] = []
|
|
|
|
pushScalar(out, 'first_name', data?.first_name, seen)
|
|
pushScalar(out, 'last_name', data?.last_name, seen)
|
|
pushScalar(out, 'company', data?.company, seen)
|
|
pushScalar(out, 'department', data?.department, seen)
|
|
pushScalar(out, 'job_title', data?.job_title, seen)
|
|
pushScalar(out, 'website', data?.website, seen)
|
|
pushLabeled(out, 'social_profiles', data?.social_profiles, seen)
|
|
pushScalar(out, 'notes', data?.notes, seen)
|
|
pushLabeled(out, 'emails', data?.emails, seen)
|
|
pushLabeled(out, 'phones', data?.phones, seen)
|
|
|
|
for (const addr of data?.addresses ?? []) {
|
|
const parts = [addr.street, addr.city, addr.region, addr.postal_code, addr.country]
|
|
.map((p) => p?.trim())
|
|
.filter(Boolean)
|
|
if (parts.length === 0) continue
|
|
const v = parts.join(', ')
|
|
pushDraftField(
|
|
out,
|
|
seen,
|
|
'addresses',
|
|
v,
|
|
addr.label ? `${FIELD_LABELS.addresses} (${addr.label})` : FIELD_LABELS.addresses,
|
|
)
|
|
}
|
|
|
|
for (const p of group.profiles ?? [profile]) {
|
|
for (const e of p.all_emails ?? []) {
|
|
const v = e.email?.trim()
|
|
if (!v) continue
|
|
pushDraftField(out, seen, 'emails', v, FIELD_LABELS.emails)
|
|
}
|
|
const primary = p.primary_email?.trim()
|
|
if (primary) {
|
|
pushDraftField(out, seen, 'emails', primary, FIELD_LABELS.emails)
|
|
}
|
|
}
|
|
|
|
return out
|
|
}
|
|
|
|
function draftFieldContactLabel(field: DraftField): string {
|
|
const base = FIELD_LABELS[field.fieldKey] ?? field.fieldKey
|
|
if (field.label.startsWith(base)) {
|
|
const suffix = field.label.slice(base.length).trim()
|
|
if (suffix.startsWith('(') && suffix.endsWith(')')) {
|
|
return suffix.slice(1, -1)
|
|
}
|
|
}
|
|
return 'Autre'
|
|
}
|
|
|
|
function parseAddressDraftValue(value: string, label: string): ContactAddress {
|
|
const parts = value
|
|
.split(',')
|
|
.map((p) => p.trim())
|
|
.filter(Boolean)
|
|
if (parts.length >= 5) {
|
|
return {
|
|
street: parts[0],
|
|
city: parts[1],
|
|
region: parts[2],
|
|
postalCode: parts[3],
|
|
country: parts.slice(4).join(', '),
|
|
label,
|
|
}
|
|
}
|
|
return { street: value, label }
|
|
}
|
|
|
|
export function applyDraftToContact(
|
|
profile: ApiDiscoveredProfile | undefined,
|
|
fields: DraftField[],
|
|
): FullContact {
|
|
if (!profile) {
|
|
throw new Error('profile is required to build contact')
|
|
}
|
|
const base = discoveredProfileToFullContact(profile)
|
|
const active = fields.filter((f) => !f.removed)
|
|
|
|
const scalar = (key: string) => active.find((f) => f.fieldKey === key)?.value
|
|
|
|
const firstName = scalar('first_name')
|
|
const lastName = scalar('last_name')
|
|
if (firstName) base.firstName = firstName
|
|
if (lastName) base.lastName = lastName
|
|
if (scalar('company')) base.company = scalar('company')
|
|
if (scalar('department')) base.department = scalar('department')
|
|
if (scalar('job_title')) base.jobTitle = scalar('job_title')
|
|
if (scalar('website')) base.website = scalar('website')
|
|
if (scalar('notes')) base.notes = scalar('notes')
|
|
|
|
const hasSocialFields = fields.some((f) => f.fieldKey === 'social_profiles')
|
|
const socialProfiles = active
|
|
.filter((f) => f.fieldKey === 'social_profiles')
|
|
.map((f) => ({
|
|
value: f.value,
|
|
label: draftFieldContactLabel(f),
|
|
}))
|
|
if (hasSocialFields) {
|
|
base.socialProfiles = socialProfiles.length > 0 ? socialProfiles : undefined
|
|
}
|
|
|
|
const hasEmailFields = fields.some((f) => f.fieldKey === 'emails')
|
|
const emails = active
|
|
.filter((f) => f.fieldKey === 'emails')
|
|
.map((f) => ({
|
|
value: f.value,
|
|
label: draftFieldContactLabel(f),
|
|
}))
|
|
if (hasEmailFields) base.emails = emails
|
|
|
|
const hasPhoneFields = fields.some((f) => f.fieldKey === 'phones')
|
|
const phones = active
|
|
.filter((f) => f.fieldKey === 'phones')
|
|
.map((f) => ({
|
|
value: f.value,
|
|
label: draftFieldContactLabel(f),
|
|
}))
|
|
if (hasPhoneFields) base.phones = phones
|
|
|
|
const hasAddressFields = fields.some((f) => f.fieldKey === 'addresses')
|
|
const addresses = active
|
|
.filter((f) => f.fieldKey === 'addresses')
|
|
.map((f) => parseAddressDraftValue(f.value, draftFieldContactLabel(f)))
|
|
if (hasAddressFields) {
|
|
base.addresses = addresses.length > 0 ? addresses : undefined
|
|
}
|
|
|
|
return base
|
|
}
|