asset_browser/public/modules/usernameUtils.js
2026-03-27 09:18:39 -04:00

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;
}