Spaces:
Sleeping
Sleeping
File size: 2,893 Bytes
ab37b00 67cca0d ab37b00 67cca0d ab37b00 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 | import { db } from '../db/index.js';
import { envelopes } from '../db/schema.js';
import { sql } from 'drizzle-orm';
/** Cached normalized name → envelope_number map */
let _cache: Map<string, string> | null = null;
/** Normalize a name for fuzzy matching: lowercase, remove accents, trim whitespace */
function normalize(name: string): string {
return name
.normalize('NFD')
.replace(/[\u0300-\u036f]/g, '')
.toLowerCase()
.replace(/\s+/g, ' ')
.trim();
}
/** Build the cache from the envelopes table */
function ensureCache(): Map<string, string> {
if (_cache) return _cache;
const rows = db.select({
envelopeNumber: envelopes.envelopeNumber,
name: envelopes.name,
}).from(envelopes).all();
_cache = new Map();
for (const row of rows) {
if (!row.name || !row.envelopeNumber) continue;
const key = normalize(row.name);
// First match wins (lower envelope numbers take priority)
if (!_cache.has(key)) {
_cache.set(key, row.envelopeNumber);
}
}
return _cache;
}
/**
* Look up the envelope number for a given sender name.
* Tries exact match first, then partial (last name) match.
*/
export function lookupEnvelopeNumber(senderName: string): string | null {
if (!senderName) return null;
const cache = ensureCache();
const normalizedSender = normalize(senderName);
// 1. Exact match
const exact = cache.get(normalizedSender);
if (exact) return exact;
// 2. Word-level match: every word in the shorter name must appear as a
// whole word in the longer name. This prevents "Anna" from matching
// "Giovanna" while still allowing "Jean Dupont" to match
// "Jean et Marie Dupont".
for (const [envName, envNum] of cache) {
const senderWords = normalizedSender.split(' ');
const envWords = envName.split(' ');
// Pick shorter set as the "query" words and the longer set as the pool
const [query, pool] =
senderWords.length <= envWords.length
? [senderWords, envWords]
: [envWords, senderWords];
if (query.length > 0 && query.every(w => pool.includes(w))) {
return envNum;
}
}
// 3. Last-name match: compare last words
const senderParts = normalizedSender.split(' ');
const senderLast = senderParts[senderParts.length - 1];
if (senderLast.length >= 3) {
for (const [envName, envNum] of cache) {
const envParts = envName.split(' ');
const envLast = envParts[envParts.length - 1];
if (envLast === senderLast) {
// Also check first name initial if available
if (senderParts.length >= 2 && envParts.length >= 2) {
if (envParts[0][0] === senderParts[0][0]) return envNum;
} else {
return envNum;
}
}
}
}
return null;
}
/** Force cache rebuild (call after CSV re-import) */
export function clearEnvelopeCache(): void {
_cache = null;
}
|