ultisuite-client/lib/contacts/merge-contacts.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

139 lines
4.4 KiB
TypeScript

import type { FullContact } from "./types"
import { fullContactDisplayName } from "./types"
function contactMergeScore(c: FullContact): number {
let score = 0
if (fullContactDisplayName(c)) score += 3
if (c.emails.some((e) => e.value.trim())) score += 4
if (c.phones.some((p) => p.value.trim())) score += 2
if (c.company?.trim()) score += 1
if (c.path?.trim()) score += 1
return score
}
export function pickContactMergePrimary(contacts: FullContact[]): FullContact | undefined {
if (contacts.length === 0) return undefined
return [...contacts].sort((a, b) => contactMergeScore(b) - contactMergeScore(a))[0]
}
export function mergeManyContacts(
contacts: FullContact[],
primaryId?: string,
): { primary: FullContact; secondaries: FullContact[]; merged: FullContact } | null {
if (contacts.length < 2) return null
const primary =
(primaryId ? contacts.find((c) => c.id === primaryId) : undefined) ??
pickContactMergePrimary(contacts)
if (!primary) return null
const secondaries = contacts.filter((c) => c.id !== primary.id)
let merged = primary
for (const secondary of secondaries) {
merged = mergeFullContactFields(merged, secondary)
}
return {
primary,
secondaries,
merged: {
...merged,
id: primary.id,
path: primary.path,
etag: primary.etag,
createdAt: primary.createdAt,
},
}
}
function dedupeLabeledValues<T extends { value: string; label: string }>(items: T[]): T[] {
const seen = new Set<string>()
const out: T[] = []
for (const item of items) {
const key = item.value.trim().toLowerCase()
if (!key || seen.has(key)) continue
seen.add(key)
out.push(item)
}
return out
}
function dedupeAddresses(items: NonNullable<FullContact["addresses"]>): NonNullable<FullContact["addresses"]> {
const seen = new Set<string>()
const out: NonNullable<FullContact["addresses"]> = []
for (const item of items) {
const key = [
item.street,
item.city,
item.region,
item.postalCode,
item.country,
item.label,
]
.map((part) => part?.trim().toLowerCase() ?? "")
.join("|")
if (!key.replace(/\|/g, "") || seen.has(key)) continue
seen.add(key)
out.push(item)
}
return out
}
function dedupeStrings(items: string[]): string[] {
const seen = new Set<string>()
const out: string[] = []
for (const item of items) {
const key = item.trim().toLowerCase()
if (!key || seen.has(key)) continue
seen.add(key)
out.push(item)
}
return out
}
function mergeNotes(a?: string, b?: string): string | undefined {
const parts = [a, b]
.map((n) => n?.trim())
.filter(Boolean) as string[]
if (parts.length === 0) return undefined
return dedupeStrings(parts).join("\n\n")
}
export function mergeFullContactFields(primary: FullContact, secondary: FullContact): FullContact {
return {
...primary,
namePrefix: primary.namePrefix || secondary.namePrefix,
firstName: primary.firstName || secondary.firstName,
middleName: primary.middleName || secondary.middleName,
lastName: primary.lastName || secondary.lastName,
nameSuffix: primary.nameSuffix || secondary.nameSuffix,
phoneticFirstName: primary.phoneticFirstName || secondary.phoneticFirstName,
phoneticLastName: primary.phoneticLastName || secondary.phoneticLastName,
company: primary.company || secondary.company,
department: primary.department || secondary.department,
jobTitle: primary.jobTitle || secondary.jobTitle,
website: primary.website || secondary.website,
notes: mergeNotes(primary.notes, secondary.notes),
emails: dedupeLabeledValues([...primary.emails, ...secondary.emails]),
phones: dedupeLabeledValues([...primary.phones, ...secondary.phones]),
addresses: dedupeAddresses([
...(primary.addresses ?? []),
...(secondary.addresses ?? []),
]),
nicknames: dedupeStrings([...(primary.nicknames ?? []), ...(secondary.nicknames ?? [])]),
labels: dedupeStrings([...(primary.labels ?? []), ...(secondary.labels ?? [])]),
avatarUrl: primary.avatarUrl || secondary.avatarUrl,
birthday: primary.birthday ?? secondary.birthday,
updatedAt: Date.now(),
}
}
export function mergeTwoContacts(
a: FullContact,
b: FullContact,
): { primary: FullContact; secondary: FullContact; merged: FullContact } {
const [primary, secondary] =
contactMergeScore(a) >= contactMergeScore(b) ? [a, b] : [b, a]
return { primary, secondary, merged: mergeFullContactFields(primary, secondary) }
}