← back to Commercialrealestate
loan-officers: always show phone+email (TK-10703)
a7b190400f33c933e7d05d4b58c5e63d749686e6 · 2026-08-18 18:26:13 -0700 · Steve Abrams
Files touched
M public/loan-officers.html
Diff
commit a7b190400f33c933e7d05d4b58c5e63d749686e6
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Tue Aug 18 18:26:13 2026 -0700
loan-officers: always show phone+email (TK-10703)
---
public/loan-officers.html | 77 +++++++++++++++++++++++++++++++++++++++++++++--
1 file changed, 75 insertions(+), 2 deletions(-)
diff --git a/public/loan-officers.html b/public/loan-officers.html
index 947bca3..579d468 100644
--- a/public/loan-officers.html
+++ b/public/loan-officers.html
@@ -64,15 +64,16 @@ window.GRID_CONFIG = {
title: 'NMLS-licensed loan officer',
api: '/api/lending/loan-officers', dataKey: 'officers',
sortKey: 'name', sortDir: 1,
- defaultCols: ['name', 'company', 'nmls_id', 'city', 'source'],
+ defaultCols: ['name', 'company', 'nmls_id', 'business_phone', 'business_email', 'city', 'source'],
cols: [
{ k: 'name', l: 'Name', t: 's', g: 'Identity' },
{ k: 'company', l: 'Company', t: 's', g: 'Identity' },
{ k: 'nmls_id', l: 'NMLS #', t: 's', g: 'Identity' },
{ k: 'title', l: 'Title', t: 's', g: 'Identity', def: 0 },
+ { k: 'business_phone', l: 'Phone', t: 'tel', g: 'Contact' },
+ { k: 'business_email', l: 'Email', t: 'mail', g: 'Contact' },
{ k: 'city', l: 'City', t: 's', g: 'Location' },
{ k: 'state', l: 'State', t: 's', g: 'Location', def: 0 },
- { k: 'business_phone', l: 'Business phone', t: 'tel', g: 'Contact', def: 0 },
{ k: 'source', l: 'Source', t: 'src', g: 'Evidence' },
{ k: 'retrieved_at', l: 'Retrieved', t: 'date', g: 'Evidence', def: 0 },
],
@@ -82,6 +83,78 @@ window.GRID_CONFIG = {
],
};
</script>
+<script>
+/* Contact enrichment (TK-10703): the loan-officers dataset is compliance-gated and may store
+ no contact of its own, so ALWAYS surface a business phone + email by joining each officer
+ client-side against /api/brokers/all by name (and firm when it disambiguates) — the same
+ brokerFor() approach used by just-listed.html. Blanks-only fill: never overwrites a real
+ value the record already carries, never fabricates one where no match exists (graceful — the
+ grid renders "—" and the Source link remains the path to full contact). Runs by wrapping
+ fetch for the officers API only, so the shared /lending-grid.js consumes enriched rows. */
+(function () {
+ var API = '/api/lending/loan-officers';
+ var norm = function (s) { return (s == null ? '' : String(s)).toLowerCase().replace(/[^a-z0-9]+/g, ' ').trim(); };
+ var brokerIndex = null; // lazily built { byName: Map, byNameFirm: Map }
+ function buildIndex(brokers) {
+ var byName = new Map(), byNameFirm = new Map();
+ (brokers || []).forEach(function (b) {
+ if (!b || !b.name) return;
+ var contact = { phone: b.phone || '', email: b.email || '' };
+ if (!contact.phone && !contact.email) return;
+ var n = norm(b.name);
+ // byName: keep first, but flag ambiguity so we don't guess when a name is shared.
+ if (byName.has(n)) { byName.get(n)._ambiguous = true; } else { byName.set(n, contact); }
+ var nf = n + '|' + norm(b.firm);
+ if (!byNameFirm.has(nf)) byNameFirm.set(nf, contact);
+ });
+ return { byName: byName, byNameFirm: byNameFirm };
+ }
+ function matchFor(o) {
+ if (!brokerIndex || !o || !o.name) return null;
+ var n = norm(o.name);
+ var nf = n + '|' + norm(o.company);
+ var m = brokerIndex.byNameFirm.get(nf); // name + firm — highest confidence
+ if (m) return m;
+ var bn = brokerIndex.byName.get(n); // name-only — only if unambiguous
+ if (bn && !bn._ambiguous) return bn;
+ return null;
+ }
+ function enrich(officers) {
+ (officers || []).forEach(function (o) {
+ if (!o) return;
+ if ((o.business_phone && o.business_email)) return; // already has both
+ var m = matchFor(o);
+ if (!m) return;
+ if (!o.business_phone && m.phone) o.business_phone = m.phone;
+ if (!o.business_email && m.email) o.business_email = m.email;
+ });
+ return officers;
+ }
+ var of = window.fetch;
+ window.fetch = function (u, opt) {
+ var url = (typeof u === 'string') ? u : (u && u.url) || '';
+ var isOfficers = url.indexOf(API) >= 0;
+ var p = of.call(this, u, opt);
+ if (!isOfficers) return p;
+ return p.then(function (res) {
+ if (!res || !res.ok) return res;
+ return res.clone().json().then(function (d) {
+ if (!d || !Array.isArray(d.officers) || !d.officers.length) return res; // nothing to enrich
+ var withBoth = d.officers.filter(function (o) { return o && o.business_phone && o.business_email; }).length;
+ if (withBoth === d.officers.length) return res; // all complete already
+ var bp = brokerIndex ? Promise.resolve(brokerIndex)
+ : of.call(window, '/api/brokers/all').then(function (r) { return r.json(); })
+ .then(function (bd) { brokerIndex = buildIndex((bd && bd.brokers) || []); return brokerIndex; })
+ .catch(function () { brokerIndex = { byName: new Map(), byNameFirm: new Map() }; return brokerIndex; });
+ return bp.then(function () {
+ enrich(d.officers);
+ return new Response(JSON.stringify(d), { status: res.status, statusText: res.statusText, headers: { 'Content-Type': 'application/json' } });
+ });
+ }).catch(function () { return res; });
+ });
+ };
+})();
+</script>
<script src="/column-manager.js" defer></script>
<script src="/lending-grid.js" defer></script>
<script src="/col-resize.js" defer></script>
← 352846c licensed-agents: always show agent phone+email (TK-10703)
·
back to Commercialrealestate
·
contractors: always show email alongside phone (TK-10703) 2681078 →