DuplicateReviewViewModel.swift 121 lignes · 3909 octets
import Combine
import Foundation

// Port of ui/contact/DuplicateReviewViewModel.kt.

@MainActor
final class DuplicateReviewViewModel: ObservableObject {
    @Published private(set) var cluster: [CrmContactEntity] = []
    @Published private(set) var selectedIds: Set<Int64> = []
    @Published private(set) var merging = false
    @Published private(set) var mergeDoneKeepId: Int64?

    let contactId: Int64

    private let repository: ContactRepository
    private var cancellables: Set<AnyCancellable> = []

    init(
        contactId: Int64,
        repository: ContactRepository = ContactRepository(
            dao: Card2vcfDatabase.shared.crmContactDao,
            images: ContactImageStore()
        )
    ) {
        self.contactId = contactId
        self.repository = repository
        NotificationCenter.default.publisher(for: .card2vcfDatabaseDidChange)
            .receive(on: RunLoop.main)
            .sink { [weak self] _ in Task { await self?.refresh() } }
            .store(in: &cancellables)
    }

    func refresh() async {
        do {
            let list = try await repository.fetchAll()
            let exclusions = try await repository.getExclusions()
            let clusters = DuplicateDetector.clusters(contacts: list, exclusions: exclusions)
            let memberIds = clusters[contactId] ?? []
            cluster = list.filter { memberIds.contains($0.id) }.sorted { $0.id < $1.id }
        } catch {
            cluster = []
        }
    }

    func toggleSelect(_ id: Int64) {
        guard id != contactId else { return }
        if selectedIds.contains(id) {
            selectedIds.remove(id)
        } else {
            selectedIds.insert(id)
        }
    }

    func deleteSelected() {
        let ids = selectedIds
        guard !ids.isEmpty else { return }
        Task {
            for id in ids {
                try? await repository.delete(id)
            }
            selectedIds = []
            await refresh()
        }
    }

    func markSelectedDifferent() {
        let ids = selectedIds
        guard !ids.isEmpty else { return }
        Task {
            for id in ids {
                try? await repository.markDifferent(contactId, id)
            }
            selectedIds = []
            await refresh()
        }
    }

    func startMerge() {
        merging = true
    }

    func cancelMerge() {
        merging = false
    }

    /// Sources = current contact + selection (or every other member when the
    /// selection is empty).
    func mergeSources(_ cluster: [CrmContactEntity]) -> [CrmContactEntity] {
        let others = cluster.filter { $0.id != contactId }
        let peers = selectedIds.isEmpty ? others : others.filter { selectedIds.contains($0.id) }
        let primary = cluster.first { $0.id == contactId }
        return (primary.map { [$0] } ?? []) + peers
    }

    func defaultChoices(_ sources: [CrmContactEntity]) -> MergeFieldChoices {
        let keep = sources.contains { $0.id == contactId } ? contactId : sources[0].id
        return MergeFieldChoices(
            fullNameFromId: keep,
            firstNameFromId: keep,
            lastNameFromId: keep,
            companyFromId: keep,
            jobTitleFromId: keep,
            websiteFromId: keep,
            addressFromId: keep,
            notesMode: .keepPrimary,
            cardImageFromId: sources.first { $0.cardImagePath?.nilIfBlank != nil }?.id,
            profileImageFromId: sources.first { $0.profileImagePath?.nilIfBlank != nil }?.id,
            keepId: keep
        )
    }

    func confirmMerge(sources: [CrmContactEntity], choices: MergeFieldChoices) {
        guard sources.count >= 2 else { return }
        Task {
            try? await repository.mergeContacts(sources: sources, choices: choices)
            merging = false
            selectedIds = []
            mergeDoneKeepId = choices.keepId
        }
    }
}