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:
parent
80e07aecbb
commit
25cf23dded
29 changed files with 947 additions and 28 deletions
|
|
@ -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) => {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue