/** * User Email Lookup Tool * Provides hover tooltips showing email addresses when hovering over user names * * @module UserEmailLookup * @description Standalone utility for looking up email addresses from the Associations * collection and displaying them as hover tooltips. Integrates with note/conversation * sharing UI to enhance user experience when managing shared access. * @usage Call window.setupUserEmailLookup(pbInstance, containerSelector) after page load. * Pass PocketBase instance and CSS selector for user capsule container. * @dependencies PocketBase instance (passed in). Browser APIs: DOM, tooltips via title attribute. * @integration Used in index.html note/conversation sharing forms to show email on hover * @gotchas Associations collection must have first_name field for lookups. Lookups are * performed on hover, so network latency may cause brief delay before tooltip appears. * Cache email lookups to minimize PocketBase queries. */ // Cache for email lookups to minimize queries const emailLookupCache = {}; let associationsCollectionAvailable = true; let associationsWarningShown = false; function getAssociationEmail(record) { return record?.emailtext || record?.email || 'Email not found'; } function handleAssociationsError(error) { const statusCode = Number(error?.status || error?.response?.status || 0); if (statusCode === 404) { associationsCollectionAvailable = false; if (!associationsWarningShown) { associationsWarningShown = true; console.warn("'Associations' collection not found. Email hover lookup disabled."); } return; } console.error('Associations lookup failed:', error); } /** * Fetch email from Associations collection by first name * Uses cache to avoid duplicate queries * * @param {PocketBase} pb - PocketBase instance * @param {string} firstName - First name to look up * @returns {Promise} Email address or fallback message * @example * const email = await getEmailByFirstName(pb, 'John'); * console.log(email); // 'john@example.com' or 'Email not found' */ async function getEmailByFirstName(pb, firstName) { if (!firstName) return 'Email not found'; if (!associationsCollectionAvailable) return 'Email lookup unavailable'; // Check cache first if (emailLookupCache[firstName]) { return emailLookupCache[firstName]; } try { const records = await pb.collection('Associations').getList(1, 50, { filter: `first_name = "${firstName}"`, }); if (records.items.length === 0) { emailLookupCache[firstName] = 'Email not found'; return 'Email not found'; } // Use first match's emailtext field const firstRecord = records.items[0]; const email = getAssociationEmail(firstRecord); emailLookupCache[firstName] = email; return email; } catch (error) { handleAssociationsError(error); return 'Error loading email'; } } /** * Setup email lookups for user capsules in a container * Attaches hover event listeners to show email in tooltip * * @param {PocketBase} pb - PocketBase instance * @param {string} containerSelector - CSS selector for container with user capsules * @example * window.setupUserEmailLookup(pb, '#selectedUsersContainer'); */ window.setupUserEmailLookup = function(pb, containerSelector) { const container = document.querySelector(containerSelector); if (!container) { console.warn(`setupUserEmailLookup: Container not found: ${containerSelector}`); return; } console.log(`✓ User Email Lookup initialized for: ${containerSelector}`); const observer = new MutationObserver((mutations) => { mutations.forEach((mutation) => { mutation.addedNodes.forEach((node) => { if (node.nodeType === 1 && node.classList?.contains('inline-flex')) { attachEmailLookupTooltip(pb, node); } }); }); }); observer.observe(container, { childList: true, subtree: true }); // Also process any existing capsules container.querySelectorAll('.inline-flex.items-center.gap-2').forEach((capsule) => { attachEmailLookupTooltip(pb, capsule); }); }; /** * Attach email lookup tooltip to a single user capsule * Shows email on hover (with caching to prevent redundant queries) * * @param {PocketBase} pb - PocketBase instance * @param {HTMLElement} capsuleEl - The user capsule element * @private */ function attachEmailLookupTooltip(pb, capsuleEl) { // Extract first name from capsule text const nameSpan = capsuleEl.querySelector('span:not([data-email-loaded])'); if (!nameSpan || nameSpan.hasAttribute('data-email-loaded')) { return; } const firstName = nameSpan.textContent.trim(); if (!firstName) return; console.log(`[Email Lookup] Attached to user: ${firstName}`); // Mark as processed nameSpan.setAttribute('data-email-loaded', 'true'); // Attach hover listener capsuleEl.addEventListener('mouseenter', async () => { console.log(`[Email Lookup] Hover on: ${firstName}`); // If email already in title, don't query again if (capsuleEl.title && !capsuleEl.title.startsWith('Loading')) { return; } capsuleEl.title = 'Loading email...'; const email = await getEmailByFirstName(pb, firstName); capsuleEl.title = email; capsuleEl.classList.add('cursor-help'); }); capsuleEl.addEventListener('mouseleave', () => { // Title persists so user can still see it if they hover back quickly }); } /** * Clear the email lookup cache * Useful if Associations collection is updated and you need fresh data * * @example * window.clearEmailLookupCache(); */ window.clearEmailLookupCache = function() { Object.keys(emailLookupCache).forEach(key => delete emailLookupCache[key]); associationsCollectionAvailable = true; associationsWarningShown = false; console.log('✓ Email lookup cache cleared'); }; console.log('✓ User Email Lookup tool loaded - call window.setupUserEmailLookup(pb, containerSelector)');