ultisuite-client/lib/contacts/discovery-draft.ts
R3D347HR4Y 07d57f13a8
Some checks are pending
E2E / Playwright e2e (push) Waiting to run
Add Contact Avatar Features and Improve UI Components
- 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.
2026-06-06 20:26:51 +02:00

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
}