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
|
|
@ -78,6 +78,20 @@ function htmlEmail(title: string, body: string): string {
|
|||
${body}
|
||||
</td>
|
||||
</tr>
|
||||
<!-- Help / support band: reduces friction & chargebacks by giving an
|
||||
obvious place to get help with any order issue. -->
|
||||
<tr>
|
||||
<td style="padding:0 40px 24px;">
|
||||
<table width="100%" cellpadding="0" cellspacing="0" style="background:#f0f9ff;border:1px solid #bae6fd;border-radius:8px;">
|
||||
<tr><td style="padding:18px 20px;text-align:center;">
|
||||
<p style="margin:0 0 4px;font-family:Arial,sans-serif;font-size:15px;font-weight:700;color:#0c4a6e;">Problem with your order? We're here to help.</p>
|
||||
<p style="margin:0 0 14px;font-family:Arial,sans-serif;font-size:13px;color:#0369a1;line-height:1.5;">Questions, a change, or something not right? Reach our team and we'll make it right, fast.</p>
|
||||
<a href="https://performancewest.net/contact" style="display:inline-block;background:#1e3a5f;color:#ffffff;padding:11px 26px;border-radius:6px;text-decoration:none;font-family:Arial,sans-serif;font-size:14px;font-weight:700;">Get help with your order →</a>
|
||||
<p style="margin:12px 0 0;font-family:Arial,sans-serif;font-size:12px;color:#64748b;">Or email <a href="mailto:info@performancewest.net" style="color:#0369a1;">info@performancewest.net</a> · call <a href="tel:18884110383" style="color:#0369a1;">1-888-411-0383</a></p>
|
||||
</td></tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
<!-- Footer -->
|
||||
<tr>
|
||||
<td style="background:#f4f5f7;border-top:1px solid #e8ecf0;padding:16px 40px;text-align:center;">
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -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