Enhance duplicate detection with sales/admin access and field updates

- Extend duplicate detection access from admin-only to sales/admin users
- Update field names for better clarity (Email → Email Address, etc.)
- Add duplicate notification banner to expenses page
- Improve authorization checks with role-based access control
This commit is contained in:
2025-07-09 15:31:55 -04:00
parent 147c8b350d
commit a337d3c838
9 changed files with 671 additions and 18 deletions

View File

@@ -1,12 +1,12 @@
import { requireAuth } from '~/server/utils/auth';
import { requireAuth, requireSalesOrAdmin } from '~/server/utils/auth';
import { getNocoDbConfiguration } from '~/server/utils/nocodb';
export default defineEventHandler(async (event) => {
console.log('[DUPLICATES] Find duplicates request');
try {
// Require authentication (any authenticated user with interest access)
await requireAuth(event);
// Require sales or admin access for duplicate detection
await requireSalesOrAdmin(event);
const query = getQuery(event);
const threshold = query.threshold ? parseFloat(query.threshold as string) : 0.8;
@@ -121,22 +121,22 @@ function calculateSimilarity(interest1: any, interest2: any) {
const scores: Array<{ type: string; score: number; weight: number }> = [];
// Email similarity (highest weight)
if (interest1.Email && interest2.Email) {
const emailScore = interest1.Email.toLowerCase() === interest2.Email.toLowerCase() ? 1.0 : 0.0;
if (interest1['Email Address'] && interest2['Email Address']) {
const emailScore = interest1['Email Address'].toLowerCase() === interest2['Email Address'].toLowerCase() ? 1.0 : 0.0;
scores.push({ type: 'email', score: emailScore, weight: 0.4 });
}
// Phone similarity
if (interest1.Phone && interest2.Phone) {
const phone1 = normalizePhone(interest1.Phone);
const phone2 = normalizePhone(interest2.Phone);
if (interest1['Phone Number'] && interest2['Phone Number']) {
const phone1 = normalizePhone(interest1['Phone Number']);
const phone2 = normalizePhone(interest2['Phone Number']);
const phoneScore = phone1 === phone2 ? 1.0 : 0.0;
scores.push({ type: 'phone', score: phoneScore, weight: 0.3 });
}
// Name similarity
if (interest1.Name && interest2.Name) {
const nameScore = calculateNameSimilarity(interest1.Name, interest2.Name);
if (interest1['Full Name'] && interest2['Full Name']) {
const nameScore = calculateNameSimilarity(interest1['Full Name'], interest2['Full Name']);
scores.push({ type: 'name', score: nameScore, weight: 0.2 });
}
@@ -237,7 +237,7 @@ function selectMasterCandidate(interests: any[]) {
* Calculate completeness score for an interest record
*/
function calculateCompletenessScore(interest: any): number {
const fields = ['Name', 'Email', 'Phone', 'Address', 'Comments', 'BerthRequirements'];
const fields = ['Full Name', 'Email Address', 'Phone Number', 'Address', 'Extra Comments', 'Berth Size Desired'];
const filledFields = fields.filter(field =>
interest[field] && interest[field].toString().trim().length > 0
);
@@ -245,8 +245,8 @@ function calculateCompletenessScore(interest: any): number {
let score = filledFields.length / fields.length;
// Bonus for recent creation
if (interest.CreatedAt) {
const created = new Date(interest.CreatedAt);
if (interest['Created At']) {
const created = new Date(interest['Created At']);
const now = new Date();
const daysOld = (now.getTime() - created.getTime()) / (1000 * 60 * 60 * 24);