feat: Refactor duplicate handling in InterestDuplicateNotificationBanner and update merge API for better access control
This commit is contained in:
parent
85ec5100f3
commit
bf24dc9103
|
|
@ -1,6 +1,6 @@
|
|||
<template>
|
||||
<!-- Debug: Always show banner for testing -->
|
||||
<v-alert
|
||||
v-if="showBanner && duplicateCount > 0"
|
||||
type="warning"
|
||||
variant="tonal"
|
||||
closable
|
||||
|
|
@ -38,7 +38,7 @@
|
|||
const { hasRole } = useAuthorization();
|
||||
|
||||
const showBanner = ref(true);
|
||||
const duplicateCount = ref(3); // Test value - should show obvious duplicates
|
||||
const duplicateCount = ref(0);
|
||||
const loading = ref(false);
|
||||
|
||||
// Check for duplicates on mount (sales/admin users)
|
||||
|
|
@ -72,8 +72,7 @@ const checkForDuplicates = async () => {
|
|||
}
|
||||
} catch (error) {
|
||||
console.error('[InterestDuplicateNotification] Failed to check for duplicates:', error);
|
||||
// For debugging, let's set a test count to see if banner shows
|
||||
duplicateCount.value = 2; // Test value to see if banner appears
|
||||
// Silently fail - this is just a notification banner
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
v-model="drawer"
|
||||
:location="mdAndDown ? 'bottom' : undefined"
|
||||
>
|
||||
<v-img v-if="!mdAndDown" src="/Port_Nimara_Logo_2_Colour_New_Transparent.png" height="75" class="my-6" />
|
||||
<v-img v-if="!mdAndDown" src="/Port_Nimara_Logo_2_Colour_New_Transparent.png" height="110" class="my-6" contain />
|
||||
|
||||
<v-list color="primary" lines="two">
|
||||
<v-list-item
|
||||
|
|
|
|||
|
|
@ -63,8 +63,8 @@
|
|||
<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' }}
|
||||
<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>
|
||||
|
|
@ -87,8 +87,8 @@
|
|||
<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' }}
|
||||
<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>
|
||||
|
|
@ -134,14 +134,15 @@ const error = ref('');
|
|||
const hasScanned = ref(false);
|
||||
const threshold = ref(80);
|
||||
|
||||
const { isAdmin } = useAuthorization();
|
||||
const { hasRole } = useAuthorization();
|
||||
|
||||
// Check admin access
|
||||
onMounted(() => {
|
||||
if (!isAdmin()) {
|
||||
// Check sales or admin access
|
||||
onMounted(async () => {
|
||||
const canAccess = await hasRole(['sales', 'admin']);
|
||||
if (!canAccess) {
|
||||
throw createError({
|
||||
statusCode: 403,
|
||||
statusMessage: 'Access denied. Admin privileges required.'
|
||||
statusMessage: 'Access denied. Sales or admin privileges required.'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
@ -152,15 +153,27 @@ const findDuplicates = async () => {
|
|||
duplicateGroups.value = [];
|
||||
|
||||
try {
|
||||
const response = await $fetch('/api/admin/duplicates/find', {
|
||||
method: 'POST',
|
||||
body: {
|
||||
threshold: threshold.value
|
||||
const response = await $fetch('/api/interests/duplicates/find', {
|
||||
method: 'GET',
|
||||
query: {
|
||||
threshold: threshold.value / 100, // Convert percentage to decimal
|
||||
dateRange: 365
|
||||
}
|
||||
});
|
||||
|
||||
if (response.success) {
|
||||
duplicateGroups.value = response.data.duplicateGroups || [];
|
||||
// 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';
|
||||
|
|
@ -179,7 +192,7 @@ const mergeDuplicates = async (group, index) => {
|
|||
try {
|
||||
const duplicateIds = group.duplicates.map(d => d.record.Id);
|
||||
|
||||
const response = await $fetch('/api/admin/duplicates/merge', {
|
||||
const response = await $fetch('/api/interests/duplicates/merge', {
|
||||
method: 'POST',
|
||||
body: {
|
||||
masterId: group.master.Id,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,189 @@
|
|||
import { requireSalesOrAdmin } from '~/server/utils/auth';
|
||||
import { getNocoDbConfiguration } from '~/server/utils/nocodb';
|
||||
import { logAuditEvent } from '~/server/utils/audit-logger';
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
console.log('[INTERESTS] Merge duplicates request');
|
||||
|
||||
let body: any;
|
||||
|
||||
try {
|
||||
// Require sales or admin access
|
||||
const session = await requireSalesOrAdmin(event);
|
||||
const userId = session.user?.id || 'unknown';
|
||||
const userEmail = session.user?.email || 'unknown';
|
||||
|
||||
body = await readBody(event);
|
||||
const { masterId, duplicateIds, mergeData } = body;
|
||||
|
||||
if (!masterId || !duplicateIds || !Array.isArray(duplicateIds) || duplicateIds.length === 0) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: 'Invalid merge request. Master ID and duplicate IDs required.'
|
||||
});
|
||||
}
|
||||
|
||||
console.log('[INTERESTS] Merging', duplicateIds.length, 'duplicates into master interest', masterId);
|
||||
|
||||
// Get NocoDB configuration
|
||||
const config = getNocoDbConfiguration();
|
||||
const interestTableId = "mbs9hjauug4eseo"; // Interest table ID
|
||||
|
||||
// First, get all interests involved
|
||||
const allIds = [masterId, ...duplicateIds];
|
||||
const interestsToMerge: any[] = [];
|
||||
|
||||
for (const id of allIds) {
|
||||
try {
|
||||
const interest = await $fetch(`${config.url}/api/v2/tables/${interestTableId}/records/${id}`, {
|
||||
headers: {
|
||||
'xc-token': config.token
|
||||
}
|
||||
});
|
||||
interestsToMerge.push(interest);
|
||||
} catch (error) {
|
||||
console.error(`[INTERESTS] Failed to fetch interest ${id}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
if (interestsToMerge.length < 2) {
|
||||
throw createError({
|
||||
statusCode: 404,
|
||||
statusMessage: 'Could not find enough interests to merge'
|
||||
});
|
||||
}
|
||||
|
||||
const masterInterest = interestsToMerge.find(i => i.Id === parseInt(masterId));
|
||||
if (!masterInterest) {
|
||||
throw createError({
|
||||
statusCode: 404,
|
||||
statusMessage: 'Master interest not found'
|
||||
});
|
||||
}
|
||||
|
||||
// Log the action before making changes
|
||||
await logAuditEvent(event, 'MERGE_INTEREST_DUPLICATES', 'interest', {
|
||||
resourceId: masterId,
|
||||
changes: {
|
||||
duplicateIds,
|
||||
mergeData,
|
||||
originalInterests: interestsToMerge
|
||||
}
|
||||
});
|
||||
|
||||
// Update master interest with merged data if provided
|
||||
if (mergeData && Object.keys(mergeData).length > 0) {
|
||||
const updateData: any = { Id: parseInt(masterId) };
|
||||
|
||||
// Copy allowed fields
|
||||
const allowedFields = [
|
||||
'Full Name',
|
||||
'Email Address',
|
||||
'Phone Number',
|
||||
'Address',
|
||||
'Extra Comments',
|
||||
'Berth Size Desired',
|
||||
'Sales Process Level',
|
||||
'EOI Status',
|
||||
'Contract Status',
|
||||
'Lead Category'
|
||||
];
|
||||
|
||||
allowedFields.forEach(field => {
|
||||
if (mergeData[field] !== undefined) {
|
||||
updateData[field] = mergeData[field];
|
||||
}
|
||||
});
|
||||
|
||||
if (Object.keys(updateData).length > 1) { // More than just Id
|
||||
console.log('[INTERESTS] Updating master interest with:', updateData);
|
||||
|
||||
await $fetch(`${config.url}/api/v2/tables/${interestTableId}/records`, {
|
||||
method: 'PATCH',
|
||||
headers: {
|
||||
'xc-token': config.token,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: updateData
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Delete duplicate interests
|
||||
const deleteResults = [];
|
||||
for (const duplicateId of duplicateIds) {
|
||||
try {
|
||||
console.log('[INTERESTS] Deleting duplicate interest:', duplicateId);
|
||||
|
||||
await $fetch(`${config.url}/api/v2/tables/${interestTableId}/records`, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'xc-token': config.token,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: { Id: parseInt(duplicateId) }
|
||||
});
|
||||
|
||||
deleteResults.push({ id: duplicateId, success: true });
|
||||
} catch (error: any) {
|
||||
console.error('[INTERESTS] Failed to delete duplicate:', duplicateId, error);
|
||||
deleteResults.push({
|
||||
id: duplicateId,
|
||||
success: false,
|
||||
error: error.message || 'Delete failed'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Check if all deletes were successful
|
||||
const failedDeletes = deleteResults.filter(r => !r.success);
|
||||
if (failedDeletes.length > 0) {
|
||||
console.warn('[INTERESTS] Some duplicates could not be deleted:', failedDeletes);
|
||||
}
|
||||
|
||||
// Log successful completion
|
||||
await logAuditEvent(event, 'MERGE_INTEREST_DUPLICATES_COMPLETE', 'interest', {
|
||||
resourceId: masterId,
|
||||
changes: {
|
||||
deletedCount: deleteResults.filter(r => r.success).length,
|
||||
failedDeletes,
|
||||
mergeData
|
||||
},
|
||||
status: 'success'
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
masterId,
|
||||
mergedData: mergeData,
|
||||
deletedCount: deleteResults.filter(r => r.success).length,
|
||||
deleteResults,
|
||||
message: `Successfully merged ${deleteResults.filter(r => r.success).length} duplicates into interest ${masterId}`
|
||||
}
|
||||
};
|
||||
|
||||
} catch (error: any) {
|
||||
console.error('[INTERESTS] Failed to merge duplicates:', error);
|
||||
|
||||
// Log the failure
|
||||
await logAuditEvent(event, 'MERGE_INTEREST_DUPLICATES_FAILED', 'interest', {
|
||||
resourceId: body?.masterId || 'unknown',
|
||||
changes: {
|
||||
error: error.message || 'Unknown error',
|
||||
requestBody: body
|
||||
},
|
||||
status: 'failure',
|
||||
errorMessage: error.message || 'Unknown error'
|
||||
});
|
||||
|
||||
if (error.statusCode) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
throw createError({
|
||||
statusCode: 500,
|
||||
statusMessage: 'Failed to merge duplicate interests'
|
||||
});
|
||||
}
|
||||
});
|
||||
Loading…
Reference in New Issue