feat(npi): add NpiIntakeStep wizard + 6 healthcare order pages

This commit is contained in:
justin 2026-06-05 01:26:58 -05:00
parent e67db156e8
commit f349d519c6
8 changed files with 415 additions and 1 deletions

View file

@ -42,6 +42,7 @@ import OCNStep from "./steps/OCNStep.astro";
import MCS150Step from "./steps/MCS150Step.astro";
import DOTIntakeStep from "./steps/DOTIntakeStep.astro";
import StateTruckingIntakeStep from "./steps/StateTruckingIntakeStep.astro";
import NpiIntakeStep from "./steps/NpiIntakeStep.astro";
import ClassificationWizard from "./steps/ClassificationWizard.astro";
import ReviewStep from "./steps/ReviewStep.astro";
import PaymentStep from "./steps/PaymentStep.astro";
@ -81,6 +82,7 @@ const STEP_LABELS: Record<string, string> = {
ocn: "OCN Details",
"dot-intake": "Carrier Details",
"state-trucking": "Filing Details",
"npi-intake": "Provider Details",
review: "Review",
payment: "Payment",
};
@ -126,6 +128,7 @@ const STEP_LABELS: Record<string, string> = {
{steps.includes("mcs150") && <div data-step="mcs150" hidden><MCS150Step /></div>}
{steps.includes("dot-intake") && <div data-step="dot-intake" hidden><DOTIntakeStep /></div>}
{steps.includes("state-trucking") && <div data-step="state-trucking" hidden><StateTruckingIntakeStep /></div>}
{steps.includes("npi-intake") && <div data-step="npi-intake" hidden><NpiIntakeStep /></div>}
{steps.includes("classification") && <div data-step="classification" hidden><ClassificationWizard /></div>}
{steps.includes("review") && <div data-step="review" hidden><ReviewStep service_slug={service_slug} /></div>}
{steps.includes("payment") && <div data-step="payment" hidden><PaymentStep service_slug={service_slug} /></div>}
@ -235,7 +238,7 @@ const STEP_LABELS: Record<string, string> = {
block6_cert: "Certifications", bdc_data: "BDC Data",
stir_shaken: "STIR/SHAKEN", calea: "CALEA",
foreign_carrier: "Foreign Affiliation", foreign_qual: "State Registration", dc_agent: "D.C. Agent", cpni_questions: "CPNI Details", cdr_period: "Reporting Period",
ocn: "OCN Details", "dot-intake": "Carrier Details", "state-trucking": "Filing Details", review: "Review", payment: "Payment",
ocn: "OCN Details", "dot-intake": "Carrier Details", "state-trucking": "Filing Details", "npi-intake": "Provider Details", review: "Review", payment: "Payment",
};
// Category-gated dynamic step insertion. After the user picks Line 105

View file

@ -0,0 +1,183 @@
---
// Healthcare / NPI provider intake. Collects the provider identity (NPI,
// name, email) plus optional PECOS / specialty / practice fields each NPI
// handler uses. Mirrors StateTruckingIntakeStep.astro's collect pattern.
---
<div class="pw-step" data-slug="npi-intake">
<h2>Provider Information</h2>
<p class="pw-help">
Tell us about the provider so we can prepare your CMS filing. We file on your behalf.
</p>
<div class="pw-form-grid">
<h3>Provider Details</h3>
<div class="pw-row-2">
<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>
</div>
<div class="pw-row-2">
<label class="pw-field"><span>Email <em>*</em></span>
<input type="email" id="npi-email" required placeholder="you@practice.com" /></label>
<label class="pw-field"><span>Practice State</span>
<select id="npi-practice-state">
<option value="">--</option>
<option>AL</option><option>AK</option><option>AZ</option><option>AR</option><option>CA</option><option>CO</option><option>CT</option><option>DE</option><option>FL</option><option>GA</option><option>HI</option><option>ID</option><option>IL</option><option>IN</option><option>IA</option><option>KS</option><option>KY</option><option>LA</option><option>ME</option><option>MD</option><option>MA</option><option>MI</option><option>MN</option><option>MS</option><option>MO</option><option>MT</option><option>NE</option><option>NV</option><option>NH</option><option>NJ</option><option>NM</option><option>NY</option><option>NC</option><option>ND</option><option>OH</option><option>OK</option><option>OR</option><option>PA</option><option>RI</option><option>SC</option><option>SD</option><option>TN</option><option>TX</option><option>UT</option><option>VT</option><option>VA</option><option>WA</option><option>WV</option><option>WI</option><option>WY</option><option>DC</option>
</select></label>
</div>
<div class="pw-row-2">
<label class="pw-field"><span>PECOS Enrollment ID <span class="pw-opt">(if known)</span></span>
<input type="text" id="npi-pecos-id" placeholder="e.g. I20040309000221" /></label>
<label class="pw-field"><span>Specialty / Taxonomy <span class="pw-opt">(optional)</span></span>
<input type="text" id="npi-specialty" placeholder="e.g. Internal Medicine" /></label>
</div>
<!-- Reactivation-specific -->
<div id="npi-sec-reactivation" hidden>
<h3>Deactivation Details</h3>
<div class="pw-row">
<label class="pw-field"><span>Reason your NPI was deactivated <span class="pw-opt">(if known)</span></span>
<input type="text" id="npi-deactivation-reason" placeholder="e.g. non-response to revalidation, voluntary, etc." /></label>
</div>
</div>
<!-- NPPES update-specific -->
<div id="npi-sec-nppes" hidden>
<h3>What needs updating?</h3>
<div class="pw-row">
<label class="pw-field"><span>Fields to update</span>
<input type="text" id="npi-fields-to-update" placeholder="e.g. practice address, phone, taxonomy, authorized official" /></label>
</div>
</div>
<!-- Screening-specific -->
<div id="npi-sec-screening" hidden>
<h3>Screening Scope</h3>
<div class="pw-row-2">
<label class="pw-field"><span>Organization Name <span class="pw-opt">(optional)</span></span>
<input type="text" id="npi-org-name" placeholder="Practice / group name" /></label>
<label class="pw-field"><span>Staff to screen <span class="pw-opt">(count)</span></span>
<input type="number" id="npi-staff-count" min="0" placeholder="e.g. 5" /></label>
</div>
</div>
</div>
<div id="pw-npi-errors" class="pw-err" hidden></div>
</div>
<style>
.pw-step h2 { color: #0f766e; margin: 0 0 0.25rem; }
.pw-help { color: #64748b; margin: 0 0 1.25rem; }
.pw-form-grid h3 { font-size: 1rem; color: #134e4a; margin: 1.25rem 0 0.5rem; border-bottom: 1px solid #ccfbf1; padding-bottom: 0.25rem; }
.pw-row { display: grid; grid-template-columns: 1fr; gap: 0.75rem; margin-bottom: 0.5rem; }
.pw-row-2 { display: grid; grid-template-columns: 1fr 1fr; gap: 0.75rem; margin-bottom: 0.5rem; }
.pw-field { display: flex; flex-direction: column; gap: 0.25rem; }
.pw-field span { font-size: 0.85rem; font-weight: 600; color: #374151; }
.pw-field em { color: #dc2626; font-style: normal; }
.pw-opt { font-weight: 400; color: #94a3b8; }
.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; }
@media (max-width: 640px) { .pw-row-2 { grid-template-columns: 1fr; } }
</style>
<script is:inline>
if (!document.querySelector('[data-slug="npi-intake"], [data-step="npi-intake"]')) {
// Not an NPI intake page — skip
} else {
// Which extra sections to show per slug.
const NPI_SECTIONS = {
"npi-reactivation": ["npi-sec-reactivation"],
"nppes-update": ["npi-sec-nppes"],
"oig-sam-screening": ["npi-sec-screening"],
"provider-compliance-bundle": ["npi-sec-screening"],
};
const ALL_NPI_SECTIONS = ["npi-sec-reactivation","npi-sec-nppes","npi-sec-screening"];
function showNpiSections() {
const PW = window.PWIntake;
const state = PW?.get?.() || {};
const wizardEl = document.querySelector(".pw-wizard[data-service]");
const pageSlug = wizardEl?.getAttribute("data-service") || "";
const slugs = state.batch_slugs || [pageSlug || state.service_slug || ""];
const show = new Set();
for (const slug of slugs) for (const sec of (NPI_SECTIONS[slug] || [])) show.add(sec);
for (const id of ALL_NPI_SECTIONS) {
const el = document.getElementById(id);
if (el) el.hidden = !show.has(id);
}
}
function initNpiSections() {
const wizardEl = document.querySelector(".pw-wizard[data-service]");
if (!wizardEl) { setTimeout(initNpiSections, 100); return; }
showNpiSections();
}
initNpiSections();
window.addEventListener("pw:step-shown", (evt) => {
if (evt.detail.step !== "npi-intake") return;
showNpiSections();
const s = window.PWIntake.get();
const d = s.intake_data || {};
const map = {
"npi-provider-name": d.provider_name || s.name || "",
"npi-number": d.npi || "",
"npi-email": d.email || s.email || "",
"npi-practice-state": d.practice_state || "",
"npi-pecos-id": d.pecos_enrollment_id || "",
"npi-specialty": d.specialty || "",
"npi-deactivation-reason": d.deactivation_reason || "",
"npi-fields-to-update": d.fields_to_update || "",
"npi-org-name": d.organization_name || "",
"npi-staff-count": d.staff_count || "",
};
for (const [id, val] of Object.entries(map)) {
const el = document.getElementById(id);
if (el && val) el.value = val;
}
});
window.addEventListener("pw:step-next", (evt) => {
const PW = window.PWIntake;
if (PW.steps[PW.get().step_index] !== "npi-intake") return;
const errDiv = document.getElementById("pw-npi-errors");
errDiv.hidden = true;
const val = (id) => (document.getElementById(id))?.value?.trim() || "";
const missing = [];
if (!val("npi-provider-name")) missing.push("Provider / Organization Name");
const npi = val("npi-number");
if (!npi) missing.push("NPI");
else if (!/^\d{10}$/.test(npi)) missing.push("NPI must be 10 digits");
const email = val("npi-email");
if (!email) missing.push("Email");
else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) missing.push("a valid Email");
if (missing.length) {
evt.preventDefault();
errDiv.hidden = false;
errDiv.textContent = "Please fix: " + missing.join(", ");
return;
}
const state = PW.get();
PW.set({ ...state, intake_data: { ...state.intake_data,
provider_name: val("npi-provider-name"),
npi: npi,
email: email,
practice_state: val("npi-practice-state"),
pecos_enrollment_id: val("npi-pecos-id"),
specialty: val("npi-specialty"),
deactivation_reason: val("npi-deactivation-reason"),
fields_to_update: val("npi-fields-to-update"),
organization_name: val("npi-org-name"),
staff_count: val("npi-staff-count"),
}});
});
} // end guard
</script>

View file

@ -0,0 +1,38 @@
---
import Base from "../../layouts/Base.astro";
import Wizard from "../../components/intake/Wizard.astro";
import TaxDeductibilityNotice from "../../components/TaxDeductibilityNotice.astro";
import { INTAKE_MANIFEST, SERVICE_META, formatUSD } from "../../lib/intake_manifest";
const slug = "medicare-enrollment";
const steps = INTAKE_MANIFEST[slug];
const meta = SERVICE_META[slug];
const title = meta ? `Order ${meta.name}` : "Order";
const description = "Complete your Medicare enrollment in PECOS (CMS-855), including taxonomy, practice location, and authorized official.";
---
<Base title={title} description={description}>
<main>
<section class="pw-order-intro">
<h1>{meta?.name}</h1>
<p class="pw-desc">{description}</p>
</section>
<Wizard service_slug={slug} steps={steps ?? ["npi-intake", "review", "payment"]} title={meta?.name ?? slug} />
</main>
<script>
// Hide price + tax on intake pages — if the user is here, they've
// already paid via the order page or batch checkout.
const price = document.getElementById("pw-price");
const tax = document.getElementById("pw-tax-notice");
if (price) price.style.display = "none";
if (tax) tax.style.display = "none";
</script>
</Base>
<style>
main { max-width: 900px; margin: 0 auto; padding: 2rem 1.25rem 4rem; }
.pw-order-intro { margin-bottom: 1.5rem; }
.pw-order-intro h1 { margin: 0 0 0.25rem; color: var(--pw-navy, #1a2744); }
.pw-desc { color: var(--pw-muted, #64748b); max-width: 48rem; }
</style>

View file

@ -0,0 +1,38 @@
---
import Base from "../../layouts/Base.astro";
import Wizard from "../../components/intake/Wizard.astro";
import TaxDeductibilityNotice from "../../components/TaxDeductibilityNotice.astro";
import { INTAKE_MANIFEST, SERVICE_META, formatUSD } from "../../lib/intake_manifest";
const slug = "npi-reactivation";
const steps = INTAKE_MANIFEST[slug];
const meta = SERVICE_META[slug];
const title = meta ? `Order ${meta.name}` : "Order";
const description = "Reactivate a deactivated NPI in NPPES so you can resume billing, with a full record re-certification.";
---
<Base title={title} description={description}>
<main>
<section class="pw-order-intro">
<h1>{meta?.name}</h1>
<p class="pw-desc">{description}</p>
</section>
<Wizard service_slug={slug} steps={steps ?? ["npi-intake", "review", "payment"]} title={meta?.name ?? slug} />
</main>
<script>
// Hide price + tax on intake pages — if the user is here, they've
// already paid via the order page or batch checkout.
const price = document.getElementById("pw-price");
const tax = document.getElementById("pw-tax-notice");
if (price) price.style.display = "none";
if (tax) tax.style.display = "none";
</script>
</Base>
<style>
main { max-width: 900px; margin: 0 auto; padding: 2rem 1.25rem 4rem; }
.pw-order-intro { margin-bottom: 1.5rem; }
.pw-order-intro h1 { margin: 0 0 0.25rem; color: var(--pw-navy, #1a2744); }
.pw-desc { color: var(--pw-muted, #64748b); max-width: 48rem; }
</style>

View file

@ -0,0 +1,38 @@
---
import Base from "../../layouts/Base.astro";
import Wizard from "../../components/intake/Wizard.astro";
import TaxDeductibilityNotice from "../../components/TaxDeductibilityNotice.astro";
import { INTAKE_MANIFEST, SERVICE_META, formatUSD } from "../../lib/intake_manifest";
const slug = "npi-revalidation";
const steps = INTAKE_MANIFEST[slug];
const meta = SERVICE_META[slug];
const title = meta ? `Order ${meta.name}` : "Order";
const description = "Prepare and file your Medicare revalidation in CMS PECOS. Revalidation is required every 5 years; missing it deactivates your billing privileges.";
---
<Base title={title} description={description}>
<main>
<section class="pw-order-intro">
<h1>{meta?.name}</h1>
<p class="pw-desc">{description}</p>
</section>
<Wizard service_slug={slug} steps={steps ?? ["npi-intake", "review", "payment"]} title={meta?.name ?? slug} />
</main>
<script>
// Hide price + tax on intake pages — if the user is here, they've
// already paid via the order page or batch checkout.
const price = document.getElementById("pw-price");
const tax = document.getElementById("pw-tax-notice");
if (price) price.style.display = "none";
if (tax) tax.style.display = "none";
</script>
</Base>
<style>
main { max-width: 900px; margin: 0 auto; padding: 2rem 1.25rem 4rem; }
.pw-order-intro { margin-bottom: 1.5rem; }
.pw-order-intro h1 { margin: 0 0 0.25rem; color: var(--pw-navy, #1a2744); }
.pw-desc { color: var(--pw-muted, #64748b); max-width: 48rem; }
</style>

View file

@ -0,0 +1,38 @@
---
import Base from "../../layouts/Base.astro";
import Wizard from "../../components/intake/Wizard.astro";
import TaxDeductibilityNotice from "../../components/TaxDeductibilityNotice.astro";
import { INTAKE_MANIFEST, SERVICE_META, formatUSD } from "../../lib/intake_manifest";
const slug = "nppes-update";
const steps = INTAKE_MANIFEST[slug];
const meta = SERVICE_META[slug];
const title = meta ? `Order ${meta.name}` : "Order";
const description = "Update and re-attest your NPPES record. CMS requires updates within 30 days of any change.";
---
<Base title={title} description={description}>
<main>
<section class="pw-order-intro">
<h1>{meta?.name}</h1>
<p class="pw-desc">{description}</p>
</section>
<Wizard service_slug={slug} steps={steps ?? ["npi-intake", "review", "payment"]} title={meta?.name ?? slug} />
</main>
<script>
// Hide price + tax on intake pages — if the user is here, they've
// already paid via the order page or batch checkout.
const price = document.getElementById("pw-price");
const tax = document.getElementById("pw-tax-notice");
if (price) price.style.display = "none";
if (tax) tax.style.display = "none";
</script>
</Base>
<style>
main { max-width: 900px; margin: 0 auto; padding: 2rem 1.25rem 4rem; }
.pw-order-intro { margin-bottom: 1.5rem; }
.pw-order-intro h1 { margin: 0 0 0.25rem; color: var(--pw-navy, #1a2744); }
.pw-desc { color: var(--pw-muted, #64748b); max-width: 48rem; }
</style>

View file

@ -0,0 +1,38 @@
---
import Base from "../../layouts/Base.astro";
import Wizard from "../../components/intake/Wizard.astro";
import TaxDeductibilityNotice from "../../components/TaxDeductibilityNotice.astro";
import { INTAKE_MANIFEST, SERVICE_META, formatUSD } from "../../lib/intake_manifest";
const slug = "oig-sam-screening";
const steps = INTAKE_MANIFEST[slug];
const meta = SERVICE_META[slug];
const title = meta ? `Order ${meta.name}` : "Order";
const description = "Annual OIG LEIE and SAM exclusion screening for you and your staff, with a compliance certificate.";
---
<Base title={title} description={description}>
<main>
<section class="pw-order-intro">
<h1>{meta?.name}</h1>
<p class="pw-desc">{description}</p>
</section>
<Wizard service_slug={slug} steps={steps ?? ["npi-intake", "review", "payment"]} title={meta?.name ?? slug} />
</main>
<script>
// Hide price + tax on intake pages — if the user is here, they've
// already paid via the order page or batch checkout.
const price = document.getElementById("pw-price");
const tax = document.getElementById("pw-tax-notice");
if (price) price.style.display = "none";
if (tax) tax.style.display = "none";
</script>
</Base>
<style>
main { max-width: 900px; margin: 0 auto; padding: 2rem 1.25rem 4rem; }
.pw-order-intro { margin-bottom: 1.5rem; }
.pw-order-intro h1 { margin: 0 0 0.25rem; color: var(--pw-navy, #1a2744); }
.pw-desc { color: var(--pw-muted, #64748b); max-width: 48rem; }
</style>

View file

@ -0,0 +1,38 @@
---
import Base from "../../layouts/Base.astro";
import Wizard from "../../components/intake/Wizard.astro";
import TaxDeductibilityNotice from "../../components/TaxDeductibilityNotice.astro";
import { INTAKE_MANIFEST, SERVICE_META, formatUSD } from "../../lib/intake_manifest";
const slug = "provider-compliance-bundle";
const steps = INTAKE_MANIFEST[slug];
const meta = SERVICE_META[slug];
const title = meta ? `Order ${meta.name}` : "Order";
const description = "Annual provider compliance bundle: revalidation monitoring, OIG/SAM screening, and NPPES upkeep in one package.";
---
<Base title={title} description={description}>
<main>
<section class="pw-order-intro">
<h1>{meta?.name}</h1>
<p class="pw-desc">{description}</p>
</section>
<Wizard service_slug={slug} steps={steps ?? ["npi-intake", "review", "payment"]} title={meta?.name ?? slug} />
</main>
<script>
// Hide price + tax on intake pages — if the user is here, they've
// already paid via the order page or batch checkout.
const price = document.getElementById("pw-price");
const tax = document.getElementById("pw-tax-notice");
if (price) price.style.display = "none";
if (tax) tax.style.display = "none";
</script>
</Base>
<style>
main { max-width: 900px; margin: 0 auto; padding: 2rem 1.25rem 4rem; }
.pw-order-intro { margin-bottom: 1.5rem; }
.pw-order-intro h1 { margin: 0 0 0.25rem; color: var(--pw-navy, #1a2744); }
.pw-desc { color: var(--pw-muted, #64748b); max-width: 48rem; }
</style>