Add duplicate management interface with scanning and merging functionality

This commit is contained in:
2025-07-09 12:32:12 -04:00
parent 4a60782f89
commit 3615e2fa9b
3 changed files with 347 additions and 210 deletions

View File

@@ -72,181 +72,119 @@ definePageMeta({
const { mdAndDown } = useDisplay();
const { user, logout, authSource } = useUnifiedAuth();
const authUtils = useAuthorization();
const { isAdmin, getUserGroups, getCurrentUser } = useAuthorization();
const tags = usePortalTags();
const drawer = ref(false);
// Safe wrapper for auth functions
const safeIsAdmin = () => {
try {
return authUtils.isAdmin();
} catch (error) {
console.error('[Dashboard] Error checking admin status:', error);
return false;
}
};
const safeGetUserGroups = () => {
try {
return authUtils.getUserGroups();
} catch (error) {
console.error('[Dashboard] Error getting user groups:', error);
return [];
}
};
const safeGetCurrentUser = () => {
try {
return authUtils.getCurrentUser();
} catch (error) {
console.error('[Dashboard] Error getting current user:', error);
return null;
}
};
// Debug auth state
onMounted(() => {
nextTick(() => {
console.log('[Dashboard] Auth state on mount:', {
isAdmin: safeIsAdmin(),
userGroups: safeGetUserGroups(),
currentUser: safeGetCurrentUser()
isAdmin: isAdmin(),
userGroups: getUserGroups(),
currentUser: getCurrentUser()
});
});
});
const interestMenu = computed(() => {
try {
const userIsAdmin = safeIsAdmin();
const userGroups = safeGetUserGroups();
console.log('[Dashboard] Computing interest menu - isAdmin:', userIsAdmin, 'groups:', userGroups);
const baseMenu = [
//{
// to: "/dashboard/interest-eoi-queue",
// icon: "mdi-tray-full",
// title: "EOI Queue",
//},
{
to: "/dashboard/interest-analytics",
icon: "mdi-view-dashboard",
title: "Analytics",
},
{
to: "/dashboard/interest-berth-list",
icon: "mdi-table",
title: "Berth List",
},
{
to: "/dashboard/interest-berth-status",
icon: "mdi-sail-boat",
title: "Berth Status",
},
{
to: "/dashboard/interest-list",
icon: "mdi-view-list",
title: "Interest List",
},
{
to: "/dashboard/interest-status",
icon: "mdi-account-check",
title: "Interest Status",
},
{
to: "/dashboard/expenses",
icon: "mdi-receipt",
title: "Expenses",
},
{
to: "/dashboard/file-browser",
icon: "mdi-folder",
title: "File Browser",
},
];
const userIsAdmin = isAdmin();
const userGroups = getUserGroups();
console.log('[Dashboard] Computing interest menu - isAdmin:', userIsAdmin, 'groups:', userGroups);
const baseMenu = [
//{
// to: "/dashboard/interest-eoi-queue",
// icon: "mdi-tray-full",
// title: "EOI Queue",
//},
{
to: "/dashboard/interest-analytics",
icon: "mdi-view-dashboard",
title: "Analytics",
},
{
to: "/dashboard/interest-berth-list",
icon: "mdi-table",
title: "Berth List",
},
{
to: "/dashboard/interest-berth-status",
icon: "mdi-sail-boat",
title: "Berth Status",
},
{
to: "/dashboard/interest-list",
icon: "mdi-view-list",
title: "Interest List",
},
{
to: "/dashboard/interest-status",
icon: "mdi-account-check",
title: "Interest Status",
},
{
to: "/dashboard/expenses",
icon: "mdi-receipt",
title: "Expenses",
},
{
to: "/dashboard/file-browser",
icon: "mdi-folder",
title: "File Browser",
},
];
// Add admin menu items if user is admin
if (userIsAdmin) {
console.log('[Dashboard] Adding admin console to interest menu');
baseMenu.push({
to: "/dashboard/admin",
icon: "mdi-shield-crown",
title: "Admin Console",
});
}
return baseMenu;
} catch (error) {
console.error('[Dashboard] Error computing interest menu:', error);
// Return basic menu without admin items on error
return [
{
to: "/dashboard/interest-analytics",
icon: "mdi-view-dashboard",
title: "Analytics",
},
{
to: "/dashboard/interest-list",
icon: "mdi-view-list",
title: "Interest List",
},
];
// Add admin menu items if user is admin
if (userIsAdmin) {
console.log('[Dashboard] Adding admin console to interest menu');
baseMenu.push({
to: "/dashboard/admin",
icon: "mdi-shield-crown",
title: "Admin Console",
});
}
return baseMenu;
});
const defaultMenu = computed(() => {
try {
const userIsAdmin = safeIsAdmin();
const userGroups = safeGetUserGroups();
console.log('[Dashboard] Computing default menu - isAdmin:', userIsAdmin, 'groups:', userGroups);
const baseMenu = [
{
to: "/dashboard/site",
icon: "mdi-view-dashboard",
title: "Site Analytics",
},
{
to: "/dashboard/data",
icon: "mdi-finance",
title: "Data Analytics",
},
{
to: "/dashboard/file-browser",
icon: "mdi-folder",
title: "File Browser",
},
];
const userIsAdmin = isAdmin();
const userGroups = getUserGroups();
console.log('[Dashboard] Computing default menu - isAdmin:', userIsAdmin, 'groups:', userGroups);
const baseMenu = [
{
to: "/dashboard/site",
icon: "mdi-view-dashboard",
title: "Site Analytics",
},
{
to: "/dashboard/data",
icon: "mdi-finance",
title: "Data Analytics",
},
{
to: "/dashboard/file-browser",
icon: "mdi-folder",
title: "File Browser",
},
];
// Add admin menu items if user is admin
if (userIsAdmin) {
console.log('[Dashboard] Adding admin console to default menu');
baseMenu.push({
to: "/dashboard/admin",
icon: "mdi-shield-crown",
title: "Admin Console",
});
}
return baseMenu;
} catch (error) {
console.error('[Dashboard] Error computing default menu:', error);
// Return basic menu without admin items on error
return [
{
to: "/dashboard/site",
icon: "mdi-view-dashboard",
title: "Site Analytics",
},
{
to: "/dashboard/data",
icon: "mdi-finance",
title: "Data Analytics",
},
];
// Add admin menu items if user is admin
if (userIsAdmin) {
console.log('[Dashboard] Adding admin console to default menu');
baseMenu.push({
to: "/dashboard/admin",
icon: "mdi-shield-crown",
title: "Admin Console",
});
}
return baseMenu;
});
const menu = computed(() =>

View File

@@ -0,0 +1,207 @@
<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'] || 'N/A' }}<br>
<strong>Phone:</strong> {{ group.master['Phone'] || 'N/A' }}
</v-col>
<v-col cols="6">
<strong>Address:</strong> {{ group.master['Address'] || 'N/A' }}<br>
<strong>Created:</strong> {{ group.master['CreatedAt'] || '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'] || 'N/A' }}<br>
<strong>Phone:</strong> {{ duplicate.record['Phone'] || 'N/A' }}
</v-col>
<v-col cols="6">
<strong>Address:</strong> {{ duplicate.record['Address'] || 'N/A' }}<br>
<strong>Created:</strong> {{ duplicate.record['CreatedAt'] || '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 { isAdmin } = useAuthorization();
// Check admin access
onMounted(() => {
if (!isAdmin()) {
throw createError({
statusCode: 403,
statusMessage: 'Access denied. Admin privileges required.'
});
}
});
const findDuplicates = async () => {
scanning.value = true;
error.value = '';
duplicateGroups.value = [];
try {
const response = await $fetch('/api/admin/duplicates/find', {
method: 'POST',
body: {
threshold: threshold.value
}
});
if (response.success) {
duplicateGroups.value = response.data.duplicateGroups || [];
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/admin/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>