Files
crewli/apps/app/src/composables/api/useIdentityMatches.ts
bert.hausmans eb1a0ac666 feat: complete person identity matching system with fuzzy detection, revert, and manual link
Implements the full identity matching engine: email matching (HIGH confidence),
fuzzy name matching with Levenshtein distance (MEDIUM confidence, upgradable to
HIGH with DOB tiebreaker), manual link/unlink, revert confirmed matches, and
automatic detection via PersonObserver. Includes 33 comprehensive tests, frontend
integration with confirm/dismiss/unlink UI, and match indicators in the persons list.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 08:44:24 +02:00

152 lines
4.1 KiB
TypeScript

import { useMutation, useQuery, useQueryClient } from '@tanstack/vue-query'
import type { MaybeRef } from 'vue'
import { unref } from 'vue'
import { apiClient } from '@/lib/axios'
import type { IdentityMatch } from '@/types/identityMatch'
import type { Person } from '@/types/person'
interface ApiResponse<T> {
success: boolean
data: T
}
interface PaginatedResponse<T> {
data: T[]
links: Record<string, string | null>
meta: {
current_page: number
per_page: number
total: number
last_page: number
}
}
interface BulkConfirmResult {
confirmed: number
errors: Array<{ match_id: string; error: string }>
}
export function useIdentityMatches(orgId: MaybeRef<string>) {
return useQuery({
queryKey: ['identity-matches', orgId],
queryFn: async () => {
const { data } = await apiClient.get<PaginatedResponse<IdentityMatch>>(
`/organisations/${unref(orgId)}/identity-matches`,
)
return data
},
enabled: () => !!unref(orgId),
})
}
export function useConfirmMatch(orgId: MaybeRef<string>) {
const qc = useQueryClient()
return useMutation({
mutationFn: async (matchId: string) => {
const { data } = await apiClient.post<ApiResponse<IdentityMatch>>(
`/organisations/${unref(orgId)}/identity-matches/${matchId}/confirm`,
)
return data.data
},
onSuccess: () => {
qc.invalidateQueries({ queryKey: ['identity-matches'] })
qc.invalidateQueries({ queryKey: ['persons'] })
},
})
}
export function useDismissMatch(orgId: MaybeRef<string>) {
const qc = useQueryClient()
return useMutation({
mutationFn: async (matchId: string) => {
const { data } = await apiClient.post<ApiResponse<IdentityMatch>>(
`/organisations/${unref(orgId)}/identity-matches/${matchId}/dismiss`,
)
return data.data
},
onSuccess: () => {
qc.invalidateQueries({ queryKey: ['identity-matches'] })
qc.invalidateQueries({ queryKey: ['persons'] })
},
})
}
export function useRevertMatch(orgId: MaybeRef<string>) {
const qc = useQueryClient()
return useMutation({
mutationFn: async (matchId: string) => {
const { data } = await apiClient.post<ApiResponse<IdentityMatch>>(
`/organisations/${unref(orgId)}/identity-matches/${matchId}/revert`,
)
return data.data
},
onSuccess: () => {
qc.invalidateQueries({ queryKey: ['identity-matches'] })
qc.invalidateQueries({ queryKey: ['persons'] })
},
})
}
export function useBulkConfirmMatches(orgId: MaybeRef<string>) {
const qc = useQueryClient()
return useMutation({
mutationFn: async (matchIds: string[]) => {
const { data } = await apiClient.post<BulkConfirmResult>(
`/organisations/${unref(orgId)}/identity-matches/bulk-confirm`,
{ match_ids: matchIds },
)
return data
},
onSuccess: () => {
qc.invalidateQueries({ queryKey: ['identity-matches'] })
qc.invalidateQueries({ queryKey: ['persons'] })
},
})
}
export function useManualLinkPerson(orgId: MaybeRef<string>, eventId: MaybeRef<string>) {
const qc = useQueryClient()
return useMutation({
mutationFn: async ({ personId, userId }: { personId: string; userId: string }) => {
const { data } = await apiClient.post<ApiResponse<IdentityMatch>>(
`/organisations/${unref(orgId)}/events/${unref(eventId)}/persons/${personId}/manual-link`,
{ user_id: userId },
)
return data.data
},
onSuccess: () => {
qc.invalidateQueries({ queryKey: ['identity-matches'] })
qc.invalidateQueries({ queryKey: ['persons'] })
},
})
}
export function useUnlinkPerson(orgId: MaybeRef<string>, eventId: MaybeRef<string>) {
const qc = useQueryClient()
return useMutation({
mutationFn: async (personId: string) => {
const { data } = await apiClient.post<ApiResponse<Person>>(
`/organisations/${unref(orgId)}/events/${unref(eventId)}/persons/${personId}/unlink`,
)
return data.data
},
onSuccess: () => {
qc.invalidateQueries({ queryKey: ['identity-matches'] })
qc.invalidateQueries({ queryKey: ['persons'] })
},
})
}