188 lines
7.8 KiB
JavaScript
Executable file
188 lines
7.8 KiB
JavaScript
Executable file
// usernameUtils.js — username normalization and fuzzy matching
|
|
|
|
// Usernames containing this string are admin/system accounts — skip display entirely
|
|
const ADMIN_PATTERN = /admin/i;
|
|
|
|
/**
|
|
* Normalizes a raw Windows/AzureAD username for display.
|
|
*
|
|
* Returns null for:
|
|
* - empty/missing values
|
|
* - admin/system accounts (username contains "admin", e.g. deRAdmin, DAdmin)
|
|
*
|
|
* Handles:
|
|
* - Domain prefix stripping: DOMAIN\user or AzureAD\KristinMcHugh
|
|
* - CamelCase splitting: KristinMcHugh → Kristin Mc Hugh
|
|
* - Word capitalization: johndaigle → Johndaigle, michael → Michael
|
|
*
|
|
* Note: all-lowercase concatenated names (johndaigle) can't be split without
|
|
* external knowledge — we just capitalize the first letter. If a contact name
|
|
* matches via fuzzy comparison the caller should prefer the contact's name.
|
|
*
|
|
* @param {string} raw — value from kabuto_information.last_user
|
|
* @returns {string|null}
|
|
*/
|
|
export function normalizeUsername(raw) {
|
|
if (!raw) return null;
|
|
|
|
// Strip domain prefix (handles both DOMAIN\user and AzureAD\Name)
|
|
let name = raw.includes('\\') ? raw.split('\\').pop() : raw;
|
|
if (!name) return null;
|
|
|
|
// Skip admin/system accounts — don't display or match these
|
|
if (ADMIN_PATTERN.test(name)) return null;
|
|
|
|
// If no spaces already, try CamelCase split
|
|
// e.g. KristinMcHugh → Kristin Mc Hugh
|
|
if (!name.includes(' ')) {
|
|
name = name.replace(/([a-z])([A-Z])/g, '$1 $2');
|
|
}
|
|
|
|
// Capitalize first letter of each word (leave remaining letters as-is to
|
|
// preserve intentional casing like McHugh after a manual split)
|
|
name = name
|
|
.split(' ')
|
|
.filter(Boolean)
|
|
.map(w => w.charAt(0).toUpperCase() + w.slice(1))
|
|
.join(' ');
|
|
|
|
return name || null;
|
|
}
|
|
|
|
/**
|
|
* Fuzzy-matches a (possibly CamelCase or space-separated) username against a
|
|
* contact's full name by stripping all spaces and comparing case-insensitively.
|
|
*
|
|
* "KristinMcHugh" ↔ "Kristin McHugh" → true (both → "kristinmchugh")
|
|
* "johndaigle" ↔ "John Daigle" → true (both → "johndaigle")
|
|
* "michael" ↔ "Michael Smith" → false (different after strip)
|
|
*
|
|
* @param {string} username — raw value from kabuto_information.last_user
|
|
* @param {string} contactName — contact full name from Syncro
|
|
* @returns {boolean}
|
|
*/
|
|
export function usernameFuzzyMatch(rawUsername, contactName) {
|
|
if (!rawUsername || !contactName) return false;
|
|
// Strip domain prefix first so DOMAIN\johndaigle compares as johndaigle
|
|
const stripDomain = s => s.includes('\\') ? s.split('\\').pop() : s;
|
|
const norm = s => stripDomain(s).toLowerCase().replace(/\s+/g, '');
|
|
return norm(rawUsername) === norm(contactName);
|
|
}
|
|
|
|
/**
|
|
* Single-name matching: if the username normalizes to a single word (e.g. "michael"),
|
|
* checks whether that word uniquely matches the first name of exactly one distinct
|
|
* contact across all contact names in the company. If so, and the assigned contact's
|
|
* first name matches, returns true.
|
|
*
|
|
* username="michael", contactName="Michael Smith", allContactNames=["Michael Smith"]
|
|
* → true (only one "michael" first name in the company)
|
|
*
|
|
* username="michael", allContactNames=["Michael Smith", "Michael Jones"]
|
|
* → false (ambiguous — two Michaels)
|
|
*
|
|
* @param {string} rawUsername — raw value from kabuto_information.last_user
|
|
* @param {string} contactName — contact full name of the assigned contact
|
|
* @param {string[]} allContactNames — all distinct contact names in the customer
|
|
* @returns {boolean}
|
|
*/
|
|
export function usernameFirstNameMatch(rawUsername, contactName, allContactNames) {
|
|
if (!rawUsername || !contactName || !allContactNames?.length) return false;
|
|
|
|
const normalized = normalizeUsername(rawUsername);
|
|
if (!normalized) return false;
|
|
|
|
// Only apply to single-word usernames (multi-word is already handled by fuzzy match)
|
|
if (normalized.includes(' ')) return false;
|
|
|
|
const lowerUser = normalized.toLowerCase();
|
|
|
|
// Assigned contact's first name must match the username
|
|
const assignedFirst = contactName.split(' ')[0].toLowerCase();
|
|
if (lowerUser !== assignedFirst) return false;
|
|
|
|
// Count how many distinct contacts share that first name
|
|
const distinct = new Set(allContactNames.map(n => n.toLowerCase()));
|
|
const matches = [...distinct].filter(cn => cn.split(' ')[0] === lowerUser);
|
|
return matches.length === 1;
|
|
}
|
|
|
|
/**
|
|
* Initial-plus-lastname matching: handles usernames like "jgallerani" that are
|
|
* constructed as first-initial + full last name.
|
|
*
|
|
* "jgallerani" ↔ "James Gallerani" → true (j + gallerani)
|
|
* "jsmith" ↔ "John Smith" → true only if unique in company
|
|
*
|
|
* Requires uniqueness across the company to avoid false positives.
|
|
*
|
|
* @param {string} rawUsername — raw value from kabuto_information.last_user
|
|
* @param {string} contactName — contact full name of the assigned contact
|
|
* @param {string[]} allContactNames — all distinct contact names in the customer
|
|
* @returns {boolean}
|
|
*/
|
|
export function usernameInitialLastNameMatch(rawUsername, contactName, allContactNames) {
|
|
if (!rawUsername || !contactName || !allContactNames?.length) return false;
|
|
|
|
const normalized = normalizeUsername(rawUsername);
|
|
if (!normalized || normalized.includes(' ')) return false;
|
|
|
|
const lowerUser = normalized.toLowerCase();
|
|
|
|
// Build pattern: first initial + last name word
|
|
// e.g. "James Gallerani" → "jgallerani", "Mary Ann Smith" → "msmith"
|
|
const _buildPattern = name => {
|
|
const parts = name.toLowerCase().split(' ').filter(Boolean);
|
|
if (parts.length < 2) return null;
|
|
return parts[0][0] + parts[parts.length - 1];
|
|
};
|
|
|
|
const assignedPattern = _buildPattern(contactName);
|
|
if (!assignedPattern || lowerUser !== assignedPattern) return false;
|
|
|
|
// Require uniqueness: prevent matching when multiple contacts share the same pattern
|
|
const distinct = new Set(allContactNames.map(n => n.toLowerCase()));
|
|
const matchCount = [...distinct].filter(cn => _buildPattern(cn) === lowerUser).length;
|
|
return matchCount === 1;
|
|
}
|
|
|
|
/**
|
|
* First-name-plus-initials matching: handles usernames like "johnd" that are
|
|
* constructed as firstname + first-letter(s) of last name parts.
|
|
*
|
|
* "johnd" ↔ "John Daigle" → true (john + d)
|
|
* "johnd" ↔ "John Davis" → still true if only one "johnd" pattern in company
|
|
* "johnd" ↔ "Jane Doe" → false (jane ≠ john)
|
|
*
|
|
* Only fires when the username is a single word and doesn't already match via
|
|
* fuzzy or first-name matching. Requires uniqueness across the company.
|
|
*
|
|
* @param {string} rawUsername — raw value from kabuto_information.last_user
|
|
* @param {string} contactName — contact full name of the assigned contact
|
|
* @param {string[]} allContactNames — all distinct contact names in the customer
|
|
* @returns {boolean}
|
|
*/
|
|
export function usernameNameInitialMatch(rawUsername, contactName, allContactNames) {
|
|
if (!rawUsername || !contactName || !allContactNames?.length) return false;
|
|
|
|
const normalized = normalizeUsername(rawUsername);
|
|
if (!normalized || normalized.includes(' ')) return false;
|
|
|
|
const lowerUser = normalized.toLowerCase();
|
|
|
|
// Build the expected pattern for the assigned contact: firstname + initials
|
|
// e.g. "John Daigle" → "johnd", "Mary Ann Smith" → "maryans"
|
|
const _buildPattern = name => {
|
|
const parts = name.toLowerCase().split(' ').filter(Boolean);
|
|
if (parts.length < 2) return null;
|
|
return parts[0] + parts.slice(1).map(p => p[0]).join('');
|
|
};
|
|
|
|
const assignedPattern = _buildPattern(contactName);
|
|
if (!assignedPattern || lowerUser !== assignedPattern) return false;
|
|
|
|
// Check uniqueness: how many contacts produce the same pattern?
|
|
const distinct = new Set(allContactNames.map(n => n.toLowerCase()));
|
|
const matchCount = [...distinct].filter(cn => _buildPattern(cn) === lowerUser).length;
|
|
return matchCount === 1;
|
|
}
|