port-nimara-client-portal/pages/dashboard/admin/duplicates.vue

221 lines
7.7 KiB
Vue

<template>
<v-container fluid>
<v-row>
<v-col>
<h1 class="text-h4 mb-4">
<v-icon class="mr-2">mdi-content-duplicate</v-icon>
Duplicate Management
</h1>
<v-card>
<v-card-title>
<v-icon class="mr-2">mdi-magnify</v-icon>
Find Duplicate Interests
</v-card-title>
<v-card-text>
<v-row align="center" class="mb-4">
<v-col cols="auto">
<v-btn
color="primary"
:loading="scanning"
@click="findDuplicates"
prepend-icon="mdi-magnify"
>
Scan for Duplicates
</v-btn>
</v-col>
<v-col cols="auto">
<v-text-field
v-model="threshold"
label="Similarity Threshold (%)"
type="number"
min="50"
max="100"
density="compact"
style="width: 200px"
/>
</v-col>
</v-row>
<v-alert v-if="error" type="error" class="mb-4">
{{ error }}
</v-alert>
<v-alert v-if="duplicateGroups.length === 0 && !scanning && hasScanned" type="success" class="mb-4">
<v-icon class="mr-2">mdi-check-circle</v-icon>
No duplicates found! Your database is clean.
</v-alert>
<v-expansion-panels v-if="duplicateGroups.length > 0" multiple>
<v-expansion-panel
v-for="(group, index) in duplicateGroups"
:key="index"
:title="`Duplicate Group ${index + 1} (${group.duplicates.length + 1} records, ${Math.round(group.similarity)}% similar)`"
>
<v-expansion-panel-text>
<v-row>
<v-col cols="12">
<h4 class="mb-3">Master Record:</h4>
<v-card variant="outlined" color="success" class="mb-4">
<v-card-text>
<v-row>
<v-col cols="6">
<strong>Name:</strong> {{ group.master['Full Name'] }}<br>
<strong>Email:</strong> {{ group.master['Email Address'] || 'N/A' }}<br>
<strong>Phone:</strong> {{ group.master['Phone Number'] || 'N/A' }}
</v-col>
<v-col cols="6">
<strong>Address:</strong> {{ group.master['Address'] || 'N/A' }}<br>
<strong>Created:</strong> {{ group.master['Created At'] || 'N/A' }}<br>
<strong>ID:</strong> {{ group.master.Id }}
</v-col>
</v-row>
</v-card-text>
</v-card>
<h4 class="mb-3">Duplicates:</h4>
<v-card
v-for="duplicate in group.duplicates"
:key="duplicate.record.Id"
variant="outlined"
color="warning"
class="mb-2"
>
<v-card-text>
<v-row>
<v-col cols="6">
<strong>Name:</strong> {{ duplicate.record['Full Name'] }}<br>
<strong>Email:</strong> {{ duplicate.record['Email Address'] || 'N/A' }}<br>
<strong>Phone:</strong> {{ duplicate.record['Phone Number'] || 'N/A' }}
</v-col>
<v-col cols="6">
<strong>Address:</strong> {{ duplicate.record['Address'] || 'N/A' }}<br>
<strong>Created:</strong> {{ duplicate.record['Created At'] || 'N/A' }}<br>
<strong>Similarity:</strong> {{ Math.round(duplicate.similarity) }}%
</v-col>
</v-row>
</v-card-text>
</v-card>
<v-divider class="my-4"></v-divider>
<v-btn
color="primary"
:loading="merging === index"
@click="mergeDuplicates(group, index)"
prepend-icon="mdi-merge"
>
Merge All Into Master
</v-btn>
</v-col>
</v-row>
</v-expansion-panel-text>
</v-expansion-panel>
</v-expansion-panels>
</v-card-text>
</v-card>
</v-col>
</v-row>
</v-container>
</template>
<script setup>
definePageMeta({
middleware: ["authentication"],
layout: false,
});
const duplicateGroups = ref([]);
const scanning = ref(false);
const merging = ref(-1);
const error = ref('');
const hasScanned = ref(false);
const threshold = ref(80);
const { hasRole } = useAuthorization();
// Check sales or admin access
onMounted(async () => {
const canAccess = await hasRole(['sales', 'admin']);
if (!canAccess) {
throw createError({
statusCode: 403,
statusMessage: 'Access denied. Sales or admin privileges required.'
});
}
});
const findDuplicates = async () => {
scanning.value = true;
error.value = '';
duplicateGroups.value = [];
try {
const response = await $fetch('/api/interests/duplicates/find', {
method: 'GET',
query: {
threshold: threshold.value / 100, // Convert percentage to decimal
dateRange: 365
}
});
if (response.success) {
// Convert the new API format to the format expected by the UI
const convertedGroups = response.data.duplicateGroups.map(group => ({
master: group.masterCandidate,
duplicates: group.interests.filter(interest => interest.Id !== group.masterCandidate.Id).map(interest => ({
record: interest,
similarity: group.confidence * 100 // Convert back to percentage
})),
similarity: group.confidence * 100,
matchReason: group.matchReason
}));
duplicateGroups.value = convertedGroups;
hasScanned.value = true;
} else {
error.value = response.error || 'Failed to find duplicates';
}
} catch (err) {
error.value = 'Failed to scan for duplicates: ' + (err.message || 'Unknown error');
} finally {
scanning.value = false;
}
};
const mergeDuplicates = async (group, index) => {
merging.value = index;
error.value = '';
try {
const duplicateIds = group.duplicates.map(d => d.record.Id);
const response = await $fetch('/api/interests/duplicates/merge', {
method: 'POST',
body: {
masterId: group.master.Id,
duplicateIds: duplicateIds,
mergeData: {} // Using master as-is for now
}
});
if (response.success) {
// Remove the merged group from the list
duplicateGroups.value.splice(index, 1);
// Show success message
const toast = useToast();
toast.success(`Successfully merged ${duplicateIds.length} duplicate records into master record ${group.master.Id}`);
} else {
error.value = response.error || 'Failed to merge duplicates';
}
} catch (err) {
error.value = 'Failed to merge duplicates: ' + (err.message || 'Unknown error');
} finally {
merging.value = -1;
}
};
</script>