Add duplicate management interface with scanning and merging functionality
This commit is contained in:
parent
4a60782f89
commit
3615e2fa9b
|
|
@ -17,72 +17,59 @@ export interface AuthState {
|
||||||
* Authorization composable for role-based access control
|
* Authorization composable for role-based access control
|
||||||
*/
|
*/
|
||||||
export const useAuthorization = () => {
|
export const useAuthorization = () => {
|
||||||
// Get the current user state from Nuxt
|
// Simple reactive state that starts with safe defaults
|
||||||
const nuxtApp = useNuxtApp();
|
const authState = reactive<AuthState>({
|
||||||
|
|
||||||
// Create reactive auth state
|
|
||||||
const authState = ref<AuthState>({
|
|
||||||
user: null,
|
user: null,
|
||||||
authenticated: false,
|
authenticated: false,
|
||||||
groups: []
|
groups: []
|
||||||
});
|
});
|
||||||
|
|
||||||
// Create a loading state
|
// Loading state
|
||||||
const isLoading = ref(true);
|
const isLoading = ref(true);
|
||||||
|
const hasInitialized = ref(false);
|
||||||
|
|
||||||
|
// Initialize auth state with comprehensive error handling
|
||||||
|
const initializeAuth = async () => {
|
||||||
|
if (hasInitialized.value) return;
|
||||||
|
|
||||||
// Function to sync auth state from nuxtApp payload
|
|
||||||
const syncAuthState = () => {
|
|
||||||
try {
|
try {
|
||||||
// Safely check if payload data exists
|
console.log('[useAuthorization] Initializing auth state...');
|
||||||
if (nuxtApp.payload && nuxtApp.payload.data && nuxtApp.payload.data.authState) {
|
|
||||||
const payloadAuthState = nuxtApp.payload.data.authState as AuthState;
|
// Try to get auth from session API directly
|
||||||
authState.value = payloadAuthState;
|
const sessionData = await $fetch('/api/auth/session') as AuthState;
|
||||||
isLoading.value = false;
|
|
||||||
console.log('[useAuthorization] Auth state synced from payload:', {
|
if (sessionData && typeof sessionData === 'object') {
|
||||||
authenticated: payloadAuthState.authenticated,
|
// Update reactive state
|
||||||
groups: payloadAuthState.groups,
|
authState.user = sessionData.user || null;
|
||||||
user: payloadAuthState.user?.email
|
authState.authenticated = sessionData.authenticated || false;
|
||||||
|
authState.groups = Array.isArray(sessionData.groups) ? sessionData.groups : [];
|
||||||
|
|
||||||
|
console.log('[useAuthorization] Auth state initialized:', {
|
||||||
|
authenticated: authState.authenticated,
|
||||||
|
groups: authState.groups,
|
||||||
|
user: authState.user?.email
|
||||||
});
|
});
|
||||||
|
|
||||||
|
hasInitialized.value = true;
|
||||||
|
isLoading.value = false;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[useAuthorization] Error syncing auth state:', error);
|
console.error('[useAuthorization] Failed to initialize auth:', error);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Set safe defaults on error
|
||||||
|
authState.user = null;
|
||||||
|
authState.authenticated = false;
|
||||||
|
authState.groups = [];
|
||||||
|
hasInitialized.value = true;
|
||||||
|
isLoading.value = false;
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Try to get auth state from API if not in payload
|
// Initialize immediately on client
|
||||||
const loadAuthState = async () => {
|
|
||||||
try {
|
|
||||||
const sessionData = await $fetch('/api/auth/session') as AuthState;
|
|
||||||
authState.value = sessionData;
|
|
||||||
isLoading.value = false;
|
|
||||||
console.log('[useAuthorization] Auth state loaded from API:', {
|
|
||||||
authenticated: sessionData.authenticated,
|
|
||||||
groups: sessionData.groups,
|
|
||||||
user: sessionData.user?.email
|
|
||||||
});
|
|
||||||
|
|
||||||
// Update nuxtApp payload for future use
|
|
||||||
updateAuthState(sessionData);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('[useAuthorization] Failed to load auth state:', error);
|
|
||||||
isLoading.value = false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Initialize auth state immediately (not just onMounted)
|
|
||||||
if (process.client) {
|
if (process.client) {
|
||||||
// Try to sync from payload first
|
initializeAuth();
|
||||||
const synced = syncAuthState();
|
|
||||||
|
|
||||||
// If not synced from payload, load from API
|
|
||||||
if (!synced) {
|
|
||||||
loadAuthState();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// On server, try to get from payload
|
|
||||||
syncAuthState();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -90,7 +77,7 @@ export const useAuthorization = () => {
|
||||||
*/
|
*/
|
||||||
const getUserGroups = (): string[] => {
|
const getUserGroups = (): string[] => {
|
||||||
try {
|
try {
|
||||||
return authState.value?.groups || [];
|
return Array.isArray(authState.groups) ? authState.groups : [];
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[useAuthorization] Error getting user groups:', error);
|
console.error('[useAuthorization] Error getting user groups:', error);
|
||||||
return [];
|
return [];
|
||||||
|
|
@ -102,7 +89,7 @@ export const useAuthorization = () => {
|
||||||
*/
|
*/
|
||||||
const getCurrentUser = (): UserWithGroups | null => {
|
const getCurrentUser = (): UserWithGroups | null => {
|
||||||
try {
|
try {
|
||||||
return authState.value?.user || null;
|
return authState.user || null;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[useAuthorization] Error getting current user:', error);
|
console.error('[useAuthorization] Error getting current user:', error);
|
||||||
return null;
|
return null;
|
||||||
|
|
@ -233,11 +220,16 @@ export const useAuthorization = () => {
|
||||||
/**
|
/**
|
||||||
* Update auth state (called by middleware)
|
* Update auth state (called by middleware)
|
||||||
*/
|
*/
|
||||||
const updateAuthState = (authState: AuthState) => {
|
const updateAuthState = (newAuthState: AuthState) => {
|
||||||
if (!nuxtApp.payload.data) {
|
try {
|
||||||
nuxtApp.payload.data = {};
|
const nuxtApp = useNuxtApp();
|
||||||
|
if (!nuxtApp.payload.data) {
|
||||||
|
nuxtApp.payload.data = {};
|
||||||
|
}
|
||||||
|
nuxtApp.payload.data.authState = newAuthState;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[useAuthorization] Error updating auth state:', error);
|
||||||
}
|
}
|
||||||
nuxtApp.payload.data.authState = authState;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
|
||||||
|
|
@ -72,181 +72,119 @@ definePageMeta({
|
||||||
|
|
||||||
const { mdAndDown } = useDisplay();
|
const { mdAndDown } = useDisplay();
|
||||||
const { user, logout, authSource } = useUnifiedAuth();
|
const { user, logout, authSource } = useUnifiedAuth();
|
||||||
const authUtils = useAuthorization();
|
const { isAdmin, getUserGroups, getCurrentUser } = useAuthorization();
|
||||||
const tags = usePortalTags();
|
const tags = usePortalTags();
|
||||||
|
|
||||||
const drawer = ref(false);
|
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
|
// Debug auth state
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
console.log('[Dashboard] Auth state on mount:', {
|
console.log('[Dashboard] Auth state on mount:', {
|
||||||
isAdmin: safeIsAdmin(),
|
isAdmin: isAdmin(),
|
||||||
userGroups: safeGetUserGroups(),
|
userGroups: getUserGroups(),
|
||||||
currentUser: safeGetCurrentUser()
|
currentUser: getCurrentUser()
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
const interestMenu = computed(() => {
|
const interestMenu = computed(() => {
|
||||||
try {
|
const userIsAdmin = isAdmin();
|
||||||
const userIsAdmin = safeIsAdmin();
|
const userGroups = getUserGroups();
|
||||||
const userGroups = safeGetUserGroups();
|
|
||||||
|
console.log('[Dashboard] Computing interest menu - isAdmin:', userIsAdmin, 'groups:', userGroups);
|
||||||
console.log('[Dashboard] Computing interest menu - isAdmin:', userIsAdmin, 'groups:', userGroups);
|
|
||||||
|
const baseMenu = [
|
||||||
const baseMenu = [
|
//{
|
||||||
//{
|
// to: "/dashboard/interest-eoi-queue",
|
||||||
// to: "/dashboard/interest-eoi-queue",
|
// icon: "mdi-tray-full",
|
||||||
// icon: "mdi-tray-full",
|
// title: "EOI Queue",
|
||||||
// title: "EOI Queue",
|
//},
|
||||||
//},
|
{
|
||||||
{
|
to: "/dashboard/interest-analytics",
|
||||||
to: "/dashboard/interest-analytics",
|
icon: "mdi-view-dashboard",
|
||||||
icon: "mdi-view-dashboard",
|
title: "Analytics",
|
||||||
title: "Analytics",
|
},
|
||||||
},
|
{
|
||||||
{
|
to: "/dashboard/interest-berth-list",
|
||||||
to: "/dashboard/interest-berth-list",
|
icon: "mdi-table",
|
||||||
icon: "mdi-table",
|
title: "Berth List",
|
||||||
title: "Berth List",
|
},
|
||||||
},
|
{
|
||||||
{
|
to: "/dashboard/interest-berth-status",
|
||||||
to: "/dashboard/interest-berth-status",
|
icon: "mdi-sail-boat",
|
||||||
icon: "mdi-sail-boat",
|
title: "Berth Status",
|
||||||
title: "Berth Status",
|
},
|
||||||
},
|
{
|
||||||
{
|
to: "/dashboard/interest-list",
|
||||||
to: "/dashboard/interest-list",
|
icon: "mdi-view-list",
|
||||||
icon: "mdi-view-list",
|
title: "Interest List",
|
||||||
title: "Interest List",
|
},
|
||||||
},
|
{
|
||||||
{
|
to: "/dashboard/interest-status",
|
||||||
to: "/dashboard/interest-status",
|
icon: "mdi-account-check",
|
||||||
icon: "mdi-account-check",
|
title: "Interest Status",
|
||||||
title: "Interest Status",
|
},
|
||||||
},
|
{
|
||||||
{
|
to: "/dashboard/expenses",
|
||||||
to: "/dashboard/expenses",
|
icon: "mdi-receipt",
|
||||||
icon: "mdi-receipt",
|
title: "Expenses",
|
||||||
title: "Expenses",
|
},
|
||||||
},
|
{
|
||||||
{
|
to: "/dashboard/file-browser",
|
||||||
to: "/dashboard/file-browser",
|
icon: "mdi-folder",
|
||||||
icon: "mdi-folder",
|
title: "File Browser",
|
||||||
title: "File Browser",
|
},
|
||||||
},
|
];
|
||||||
];
|
|
||||||
|
|
||||||
// Add admin menu items if user is admin
|
// Add admin menu items if user is admin
|
||||||
if (userIsAdmin) {
|
if (userIsAdmin) {
|
||||||
console.log('[Dashboard] Adding admin console to interest menu');
|
console.log('[Dashboard] Adding admin console to interest menu');
|
||||||
baseMenu.push({
|
baseMenu.push({
|
||||||
to: "/dashboard/admin",
|
to: "/dashboard/admin",
|
||||||
icon: "mdi-shield-crown",
|
icon: "mdi-shield-crown",
|
||||||
title: "Admin Console",
|
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",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return baseMenu;
|
||||||
});
|
});
|
||||||
|
|
||||||
const defaultMenu = computed(() => {
|
const defaultMenu = computed(() => {
|
||||||
try {
|
const userIsAdmin = isAdmin();
|
||||||
const userIsAdmin = safeIsAdmin();
|
const userGroups = getUserGroups();
|
||||||
const userGroups = safeGetUserGroups();
|
|
||||||
|
console.log('[Dashboard] Computing default menu - isAdmin:', userIsAdmin, 'groups:', userGroups);
|
||||||
console.log('[Dashboard] Computing default menu - isAdmin:', userIsAdmin, 'groups:', userGroups);
|
|
||||||
|
const baseMenu = [
|
||||||
const baseMenu = [
|
{
|
||||||
{
|
to: "/dashboard/site",
|
||||||
to: "/dashboard/site",
|
icon: "mdi-view-dashboard",
|
||||||
icon: "mdi-view-dashboard",
|
title: "Site Analytics",
|
||||||
title: "Site Analytics",
|
},
|
||||||
},
|
{
|
||||||
{
|
to: "/dashboard/data",
|
||||||
to: "/dashboard/data",
|
icon: "mdi-finance",
|
||||||
icon: "mdi-finance",
|
title: "Data Analytics",
|
||||||
title: "Data Analytics",
|
},
|
||||||
},
|
{
|
||||||
{
|
to: "/dashboard/file-browser",
|
||||||
to: "/dashboard/file-browser",
|
icon: "mdi-folder",
|
||||||
icon: "mdi-folder",
|
title: "File Browser",
|
||||||
title: "File Browser",
|
},
|
||||||
},
|
];
|
||||||
];
|
|
||||||
|
|
||||||
// Add admin menu items if user is admin
|
// Add admin menu items if user is admin
|
||||||
if (userIsAdmin) {
|
if (userIsAdmin) {
|
||||||
console.log('[Dashboard] Adding admin console to default menu');
|
console.log('[Dashboard] Adding admin console to default menu');
|
||||||
baseMenu.push({
|
baseMenu.push({
|
||||||
to: "/dashboard/admin",
|
to: "/dashboard/admin",
|
||||||
icon: "mdi-shield-crown",
|
icon: "mdi-shield-crown",
|
||||||
title: "Admin Console",
|
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",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return baseMenu;
|
||||||
});
|
});
|
||||||
|
|
||||||
const menu = computed(() =>
|
const menu = computed(() =>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
Loading…
Reference in New Issue