Task 5.23: Deduplication / Merge — ContactMergeHistory model, dedup service, API routes (duplicates/merge/merge-history), DedupDialog frontend, i18n, 5 tests

This commit is contained in:
Agent Zero
2026-07-23 23:58:45 +02:00
parent a914280a4e
commit d54a87cf84
11 changed files with 1051 additions and 3 deletions
+88
View File
@@ -0,0 +1,88 @@
/**
* Deduplication / merge hooks for contacts (Task 5.23).
*/
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { apiPost, apiGet } from './client';
export interface DuplicateContactBrief {
id: string;
type: string;
displayname: string;
name: string | null;
firstname: string | null;
surname: string | null;
email_1: string | null;
email_2: string | null;
phone_1: string | null;
phone_2: string | null;
mailing_city: string | null;
mailing_postalcode: string | null;
created_at: string | null;
}
export interface DuplicatePair {
source_contact: DuplicateContactBrief;
target_contact: DuplicateContactBrief;
similarity_score: number;
match_reasons: string[];
}
export interface MergeHistoryItem {
id: string;
source_contact_id: string;
target_contact_id: string;
merged_fields: Record<string, unknown>;
merged_by: string | null;
note: string | null;
created_at: string | null;
}
export interface MergeHistoryResponse {
items: MergeHistoryItem[];
total: number;
page: number;
page_size: number;
}
export interface MergeRequest {
source_contact_id: string;
target_contact_id: string;
field_overrides?: Record<string, unknown>;
note?: string;
}
export function useFindDuplicates() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (params: { threshold?: number; limit?: number }) =>
apiPost<DuplicatePair[]>('/contacts/duplicates', {
threshold: params.threshold ?? 0.7,
limit: params.limit ?? 50,
}),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['duplicates'] });
},
});
}
export function useMergeContacts() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (data: MergeRequest) =>
apiPost('/contacts/merge', data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['contacts'] });
queryClient.invalidateQueries({ queryKey: ['duplicates'] });
queryClient.invalidateQueries({ queryKey: ['mergeHistory'] });
},
});
}
export function useMergeHistory(page = 1, pageSize = 20) {
return useQuery({
queryKey: ['mergeHistory', page, pageSize],
queryFn: () =>
apiGet<MergeHistoryResponse>(`/contacts/merge-history?page=${page}&page_size=${pageSize}`),
});
}