feat(orders): reduce friction & chargebacks across order flow

1. Email: add a 'Problem with your order? We're here to help' support band to
   the shared htmlEmail() footer, so EVERY transactional email (confirmation,
   portal link, receipts) has a prominent 'Get help with your order' button
   linking to /contact. Less silent frustration -> fewer chargebacks.

2. NPI order form: entering a 10-digit NPI now auto-fills provider name, practice
   state, and specialty from the live NPPES lookup (same API as the free
   compliance-check tool), with a 'Found: <name>' confirmation. Only fills empty
   fields so it never clobbers edits.

3. NPI order form: read ?npi= from the URL so the email 'Start my revalidation'
   click lands with the NPI prefilled and the rest auto-filled (was being
   ignored entirely before).

4. Support FAB: add the floating help button + panel to 27 static public pages
   that were missing it (order, portal, trucking, survey, upload pages), so help
   is one click away everywhere.
This commit is contained in:
justin 2026-06-08 00:24:17 -05:00
parent 80e07aecbb
commit 25cf23dded
29 changed files with 947 additions and 28 deletions

View file

@ -16,7 +16,8 @@
<label class="pw-field"><span>Provider / Organization Name <em>*</em></span>
<input type="text" id="npi-provider-name" required placeholder="As registered in NPPES" /></label>
<label class="pw-field"><span>NPI (10-digit) <em>*</em></span>
<input type="text" id="npi-number" required maxlength="10" inputmode="numeric" placeholder="e.g. 1234567893" /></label>
<input type="text" id="npi-number" required maxlength="10" inputmode="numeric" placeholder="e.g. 1234567893" />
<span id="npi-lookup-status" class="pw-lookup-status" hidden></span></label>
</div>
<div class="pw-row-2">
<label class="pw-field"><span>Email <em>*</em></span>
@ -101,6 +102,10 @@
.pw-field input, .pw-field select { padding: 0.55rem 0.7rem; border: 1px solid #cbd5e1; border-radius: 6px; font-size: 0.95rem; }
.pw-field input:focus, .pw-field select:focus { outline: none; border-color: #14b8a6; box-shadow: 0 0 0 2px rgba(20,184,166,0.2); }
.pw-err { color: #b91c1c; margin-top: 0.75rem; font-size: 0.9rem; background: #fee2e2; padding: 0.5rem 0.75rem; border-radius: 6px; }
.pw-lookup-status { font-size: 0.8rem; margin-top: 0.15rem; min-height: 1rem; }
.pw-lookup-status.is-loading { color: #0d9488; }
.pw-lookup-status.is-ok { color: #047857; }
.pw-lookup-status.is-err { color: #b45309; }
@media (max-width: 640px) { .pw-row-2 { grid-template-columns: 1fr; } }
</style>
@ -120,6 +125,9 @@
};
const ALL_NPI_SECTIONS = ["npi-sec-reactivation","npi-sec-nppes","npi-sec-screening","npi-sec-surrogate"];
// NPPES auto-fill state (declared before init runs to avoid TDZ).
let npiLookupDone = "";
function showNpiSections() {
const PW = window.PWIntake;
const state = PW?.get?.() || {};
@ -138,9 +146,76 @@
const wizardEl = document.querySelector(".pw-wizard[data-service]");
if (!wizardEl) { setTimeout(initNpiSections, 100); return; }
showNpiSections();
// First step is visible on load (no step-shown event yet), so wire + prefill
// here too for the email "?npi=" deep-link landing directly on this step.
wireNpiAutofill();
const npiInput = document.getElementById("npi-number");
const urlNpi = npiFromUrl();
if (npiInput && !npiInput.value.trim() && urlNpi) npiInput.value = urlNpi;
const cur = (npiInput && npiInput.value.trim()) || "";
if (/^\d{10}$/.test(cur)) autofillFromNpi(cur, false);
}
initNpiSections();
// ── NPPES auto-fill ────────────────────────────────────────────────
// Entering a valid 10-digit NPI looks the provider up in NPPES (same API
// that powers the free compliance-check tool) and auto-fills the name,
// practice state, and specialty so the provider doesn't retype what we
// already know. Only fills empty fields so we never clobber edits.
function setLookupStatus(msg, cls) {
const el = document.getElementById("npi-lookup-status");
if (!el) return;
el.hidden = !msg;
el.textContent = msg || "";
el.className = "pw-lookup-status" + (cls ? " " + cls : "");
}
async function autofillFromNpi(npi, force) {
if (!/^\d{10}$/.test(npi)) return;
if (npiLookupDone === npi && !force) return;
npiLookupDone = npi;
setLookupStatus("Looking up your NPI in NPPES\u2026", "is-loading");
try {
const res = await fetch("/api/v1/npi/lookup?npi=" + encodeURIComponent(npi), { headers: { Accept: "application/json" } });
if (!res.ok) throw new Error("lookup failed");
const d = await res.json();
const fill = (id, val) => {
const el = document.getElementById(id);
if (el && val && !el.value.trim()) { el.value = val; return true; }
return false;
};
fill("npi-provider-name", d.name);
fill("npi-practice-state", d.practice_state);
fill("npi-specialty", d.taxonomy && d.taxonomy.desc);
if (d.name) {
setLookupStatus("\u2713 Found: " + d.name + (d.practice_state ? " (" + d.practice_state + ")" : ""), "is-ok");
} else {
setLookupStatus("", "");
}
} catch (e) {
// Non-fatal: provider can still type it in manually.
setLookupStatus("Couldn\u2019t auto-fill \u2014 please enter your details below.", "is-err");
npiLookupDone = ""; // allow retry
}
}
function wireNpiAutofill() {
const input = document.getElementById("npi-number");
if (!input || input.dataset.pwAutofill) return;
input.dataset.pwAutofill = "1";
const trigger = () => autofillFromNpi(input.value.trim(), false);
input.addEventListener("blur", trigger);
input.addEventListener("input", () => {
if (/^\d{10}$/.test(input.value.trim())) trigger();
});
}
// Pull NPI from the URL (?npi=...) so an email "Start my revalidation" click
// lands with the NPI prefilled and the rest auto-filled.
function npiFromUrl() {
try { return (new URLSearchParams(location.search).get("npi") || "").replace(/\D/g, "").slice(0, 10); }
catch (_) { return ""; }
}
window.addEventListener("pw:step-shown", (evt) => {
if (evt.detail.step !== "npi-intake") return;
showNpiSections();
@ -163,6 +238,15 @@
const el = document.getElementById(id);
if (el && val) el.value = val;
}
// Wire NPPES auto-fill, then prefill the NPI from saved state or the URL
// (?npi=) and run the lookup so name/state/specialty fill in automatically.
wireNpiAutofill();
const npiInput = document.getElementById("npi-number");
const urlNpi = npiFromUrl();
if (npiInput && !npiInput.value.trim() && urlNpi) npiInput.value = urlNpi;
const currentNpi = (npiInput && npiInput.value.trim()) || "";
if (/^\d{10}$/.test(currentNpi)) autofillFromNpi(currentNpi, false);
});
window.addEventListener("pw:step-next", (evt) => {