When the intake page is loaded with ?order=CO-xxx (from the confirmation email), fetch the order and pre-fill customer name, email, and FRN from the order record. Previously only worked with JWT token-based links. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
554 lines
24 KiB
Text
554 lines
24 KiB
Text
---
|
|
/**
|
|
* Intake Wizard shell.
|
|
*
|
|
* Renders a step progress bar + back/next buttons; the active step's
|
|
* content is rendered inline from the matching component under
|
|
* ./steps/<kebab>Step.astro via dynamic import.
|
|
*
|
|
* Props:
|
|
* service_slug: which catalog entry this wizard targets
|
|
* steps: ordered list from INTAKE_MANIFEST[slug]
|
|
* title: page title shown above the wizard
|
|
*
|
|
* State lives in window.sessionStorage under the key
|
|
* "pw-intake-<slug>" so a reload doesn't wipe progress. Step components
|
|
* read/write via the shared `PWIntake` browser helper declared below.
|
|
*/
|
|
|
|
import EntityStep from "./steps/EntityStep.astro";
|
|
import CategoryStep from "./steps/CategoryStep.astro";
|
|
import OfficerStep from "./steps/OfficerStep.astro";
|
|
import JurisdictionStep from "./steps/JurisdictionStep.astro";
|
|
import HistoryStep from "./steps/HistoryStep.astro";
|
|
import WirelessStep from "./steps/WirelessStep.astro";
|
|
import EarthStationStep from "./steps/EarthStationStep.astro";
|
|
import AudioBridgingStep from "./steps/AudioBridgingStep.astro";
|
|
import RevenueStep from "./steps/RevenueStep.astro";
|
|
import BundledServiceStep from "./steps/BundledServiceStep.astro";
|
|
import IccImportStep from "./steps/IccImportStep.astro";
|
|
import ResellerCertStep from "./steps/ResellerCertStep.astro";
|
|
import LNPARegionStep from "./steps/LNPARegionStep.astro";
|
|
import Block6CertStep from "./steps/Block6CertStep.astro";
|
|
import BDCDataStep from "./steps/BDCDataStep.astro";
|
|
import STIRShakenStep from "./steps/STIRShakenStep.astro";
|
|
import CALEAStep from "./steps/CALEAStep.astro";
|
|
import ForeignCarrierStep from "./steps/ForeignCarrierStep.astro";
|
|
import ForeignQualStep from "./steps/ForeignQualStep.astro";
|
|
import DCAgentStep from "./steps/DCAgentStep.astro";
|
|
import CPNIStep from "./steps/CPNIStep.astro";
|
|
import CDRPeriodStep from "./steps/CDRPeriodStep.astro";
|
|
import OCNStep from "./steps/OCNStep.astro";
|
|
import ClassificationWizard from "./steps/ClassificationWizard.astro";
|
|
import ReviewStep from "./steps/ReviewStep.astro";
|
|
import PaymentStep from "./steps/PaymentStep.astro";
|
|
|
|
export interface Props {
|
|
service_slug: string;
|
|
steps: string[];
|
|
title: string;
|
|
}
|
|
|
|
const { service_slug, steps, title } = Astro.props;
|
|
|
|
const STEP_LABELS: Record<string, string> = {
|
|
entity: "Carrier",
|
|
category: "Line 105",
|
|
classification: "Carrier Type",
|
|
officer: "Officers",
|
|
jurisdiction: "Jurisdictions",
|
|
history: "Service Start",
|
|
wireless: "Wireless",
|
|
earth_station: "Satellite / Pvt Line",
|
|
audio_bridging: "Audio Bridging",
|
|
revenue: "Revenue",
|
|
bundled_service: "Bundles",
|
|
icc_import: "ICC Import",
|
|
reseller_cert: "Resellers",
|
|
lnpa_region: "LNPA Regions",
|
|
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",
|
|
review: "Review",
|
|
payment: "Payment",
|
|
};
|
|
---
|
|
|
|
<div class="pw-wizard" data-service={service_slug} data-steps={JSON.stringify(steps)}>
|
|
<header class="pw-wizard-header">
|
|
<h1>{title}</h1>
|
|
<ol class="pw-wizard-stepbar">
|
|
{steps.map((step, i) => (
|
|
<li class="pw-step-chip" data-step={step} data-idx={i}>
|
|
<span class="pw-step-num">{i + 1}</span>
|
|
<span class="pw-step-label">{STEP_LABELS[step] ?? step}</span>
|
|
</li>
|
|
))}
|
|
</ol>
|
|
</header>
|
|
|
|
<section class="pw-wizard-body">
|
|
{steps.includes("entity") && <div data-step="entity" hidden><EntityStep /></div>}
|
|
{steps.includes("category") && <div data-step="category" hidden><CategoryStep /></div>}
|
|
{steps.includes("officer") && <div data-step="officer" hidden><OfficerStep /></div>}
|
|
{steps.includes("jurisdiction") && <div data-step="jurisdiction" hidden><JurisdictionStep /></div>}
|
|
{steps.includes("history") && <div data-step="history" hidden><HistoryStep /></div>}
|
|
<div data-step="wireless" hidden><WirelessStep /></div>
|
|
<div data-step="earth_station" hidden><EarthStationStep /></div>
|
|
<div data-step="audio_bridging" hidden><AudioBridgingStep /></div>
|
|
{steps.includes("revenue") && <div data-step="revenue" hidden><RevenueStep /></div>}
|
|
{steps.includes("bundled_service") && <div data-step="bundled_service" hidden><BundledServiceStep /></div>}
|
|
{steps.includes("icc_import") && <div data-step="icc_import" hidden><IccImportStep /></div>}
|
|
{steps.includes("reseller_cert") && <div data-step="reseller_cert" hidden><ResellerCertStep /></div>}
|
|
{steps.includes("lnpa_region") && <div data-step="lnpa_region" hidden><LNPARegionStep /></div>}
|
|
{steps.includes("block6_cert") && <div data-step="block6_cert" hidden><Block6CertStep /></div>}
|
|
{steps.includes("bdc_data") && <div data-step="bdc_data" hidden><BDCDataStep service_slug={service_slug} /></div>}
|
|
{steps.includes("stir_shaken") && <div data-step="stir_shaken" hidden><STIRShakenStep /></div>}
|
|
{steps.includes("calea") && <div data-step="calea" hidden><CALEAStep /></div>}
|
|
{steps.includes("foreign_carrier") && <div data-step="foreign_carrier" hidden><ForeignCarrierStep /></div>}
|
|
{steps.includes("foreign_qual") && <div data-step="foreign_qual" hidden><ForeignQualStep /></div>}
|
|
{steps.includes("dc_agent") && <div data-step="dc_agent" hidden><DCAgentStep /></div>}
|
|
{steps.includes("cpni_questions") && <div data-step="cpni_questions" hidden><CPNIStep /></div>}
|
|
{steps.includes("cdr_period") && <div data-step="cdr_period" hidden><CDRPeriodStep /></div>}
|
|
{steps.includes("ocn") && <div data-step="ocn" hidden><OCNStep /></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>}
|
|
</section>
|
|
|
|
<footer class="pw-wizard-nav">
|
|
<button type="button" class="pw-btn pw-btn-plain" id="pw-back">← Back</button>
|
|
<button type="button" class="pw-btn" id="pw-next">Next →</button>
|
|
</footer>
|
|
</div>
|
|
|
|
<style is:global>
|
|
.pw-wizard { max-width: 820px; margin: 0 auto; padding: 1rem; }
|
|
.pw-prefill-notice {
|
|
background: #fef3c7; border: 1px solid #fbbf24; border-radius: 8px;
|
|
padding: 0.6rem 0.85rem; margin-bottom: 1rem; font-size: 0.82rem;
|
|
color: #92400e; line-height: 1.5;
|
|
}
|
|
.pw-wizard-header h1 { margin: 0 0 1rem; color: var(--pw-navy, #1a2744); }
|
|
.pw-wizard-stepbar {
|
|
display: flex; align-items: center; flex-wrap: wrap;
|
|
padding: 0; margin: 0 0 1.5rem; list-style: none;
|
|
gap: 0; background: #f8fafc; border-radius: 8px;
|
|
border: 1px solid #e2e8f0; overflow: hidden;
|
|
}
|
|
.pw-step-chip {
|
|
display: flex; align-items: center; gap: 0.35rem;
|
|
padding: 0.5rem 0.9rem;
|
|
color: #94a3b8;
|
|
font-size: 0.8rem;
|
|
transition: all 0.15s;
|
|
position: relative;
|
|
white-space: nowrap;
|
|
}
|
|
.pw-step-chip:not(:last-child)::after {
|
|
content: "";
|
|
position: absolute; right: -0.5rem; top: 50%;
|
|
transform: translateY(-50%) rotate(45deg);
|
|
width: 0.7rem; height: 0.7rem;
|
|
border-top: 1px solid #cbd5e1;
|
|
border-right: 1px solid #cbd5e1;
|
|
background: #f8fafc;
|
|
z-index: 1;
|
|
}
|
|
.pw-step-chip[data-active="true"] { background: #1a2744; color: #fff; }
|
|
.pw-step-chip[data-active="true"]::after { background: #1a2744; border-color: #1a2744; }
|
|
.pw-step-chip[data-done="true"] { color: #065f46; }
|
|
.pw-step-chip[data-done="true"]::after { border-color: #a7f3d0; }
|
|
.pw-step-num {
|
|
display: inline-flex; align-items: center; justify-content: center;
|
|
width: 1.2rem; height: 1.2rem; border-radius: 50%;
|
|
background: #e2e8f0; color: #64748b;
|
|
font-size: 0.7rem; font-weight: 700;
|
|
}
|
|
.pw-step-chip[data-active="true"] .pw-step-num { background: rgba(255,255,255,0.25); color: #fff; }
|
|
.pw-step-chip[data-done="true"] .pw-step-num { background: #d1fae5; color: #065f46; }
|
|
.pw-wizard-body {
|
|
background: #fff; border: 1px solid #e2e8f0; border-radius: 10px;
|
|
padding: 1.5rem; min-height: 280px;
|
|
}
|
|
.pw-wizard-nav {
|
|
display: flex; justify-content: space-between;
|
|
margin-top: 1.25rem;
|
|
}
|
|
.pw-btn {
|
|
padding: 0.65rem 1.4rem; border: 0; border-radius: 6px;
|
|
background: #059669; color: #fff; font-weight: 600;
|
|
cursor: pointer; font-size: 0.95rem;
|
|
}
|
|
.pw-btn-plain { background: #e2e8f0; color: #1f2937; }
|
|
.pw-btn:disabled { opacity: 0.5; cursor: not-allowed; }
|
|
</style>
|
|
|
|
<script>
|
|
// ── Client state machine + sessionStorage persistence ──────────────
|
|
type IntakeState = {
|
|
service_slug: string;
|
|
step_index: number;
|
|
entity: Record<string, any>;
|
|
officers: Record<string, any>[];
|
|
intake_data: Record<string, any>;
|
|
email: string;
|
|
name: string;
|
|
telecom_entity_id: number | null;
|
|
};
|
|
|
|
const wizard = document.querySelector(".pw-wizard")!;
|
|
const slug = wizard.getAttribute("data-service")!;
|
|
const initialSteps: string[] = JSON.parse(wizard.getAttribute("data-steps")!);
|
|
let steps: string[] = [...initialSteps];
|
|
const storageKey = `pw-intake-${slug}`;
|
|
|
|
// Step-label map used by the dynamic step bar rebuilder when
|
|
// line_105_categories changes. Mirror of the Astro-side STEP_LABELS.
|
|
(window as any).PW_STEP_LABELS = {
|
|
entity: "Carrier", category: "Line 105", officer: "Officers",
|
|
jurisdiction: "Jurisdictions", history: "Service Start",
|
|
wireless: "Wireless", earth_station: "Satellite / Pvt Line",
|
|
audio_bridging: "Audio Bridging", revenue: "Revenue",
|
|
bundled_service: "Bundles", icc_import: "ICC Import",
|
|
reseller_cert: "Resellers", lnpa_region: "LNPA Regions",
|
|
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", review: "Review", payment: "Payment",
|
|
};
|
|
|
|
// Category-gated dynamic step insertion. After the user picks Line 105
|
|
// categories on the CategoryStep, this re-shapes steps[] to insert
|
|
// wireless / earth_station / audio_bridging between category and
|
|
// revenue (or between entity and revenue if no category step).
|
|
const CATEGORY_GATED: Record<string, string[]> = {
|
|
wireless: ["wireless"],
|
|
satellite: ["earth_station"],
|
|
mobile_satellite: ["earth_station"],
|
|
private_line: ["earth_station"],
|
|
audio_bridging: ["audio_bridging"],
|
|
};
|
|
|
|
function reshapeSteps(categories: Array<{id: string}>): string[] {
|
|
const base = [...initialSteps];
|
|
const gated = new Set<string>();
|
|
for (const cat of categories) {
|
|
for (const step of (CATEGORY_GATED[cat.id] || [])) {
|
|
gated.add(step);
|
|
}
|
|
}
|
|
if (gated.size === 0) return base;
|
|
// Insert gated steps after `history` if present, else after `category`,
|
|
// else before `revenue`.
|
|
const anchorIdx = Math.max(
|
|
base.indexOf("history"),
|
|
base.indexOf("category"),
|
|
);
|
|
const insertAt = anchorIdx >= 0 ? anchorIdx + 1 : base.indexOf("revenue");
|
|
if (insertAt < 0) return base;
|
|
return [
|
|
...base.slice(0, insertAt),
|
|
...Array.from(gated).filter((s) => !base.includes(s)),
|
|
...base.slice(insertAt),
|
|
];
|
|
}
|
|
|
|
function rebuildStepBar() {
|
|
const bar = wizard.querySelector(".pw-wizard-stepbar")!;
|
|
bar.innerHTML = "";
|
|
for (let i = 0; i < steps.length; i++) {
|
|
const li = document.createElement("li");
|
|
li.className = "pw-step-chip";
|
|
li.setAttribute("data-step", steps[i]);
|
|
li.setAttribute("data-idx", String(i));
|
|
const label = (window as any).PW_STEP_LABELS?.[steps[i]] ?? steps[i];
|
|
li.innerHTML = `<span class="pw-step-num">${i + 1}</span><span class="pw-step-label">${label}</span>`;
|
|
bar.appendChild(li);
|
|
}
|
|
}
|
|
|
|
function loadState(): IntakeState {
|
|
try {
|
|
const raw = sessionStorage.getItem(storageKey);
|
|
if (raw) return JSON.parse(raw);
|
|
} catch {}
|
|
return {
|
|
service_slug: slug,
|
|
step_index: 0,
|
|
entity: {},
|
|
officers: [{}, {}, {}],
|
|
intake_data: {},
|
|
email: "",
|
|
name: "",
|
|
telecom_entity_id: null,
|
|
};
|
|
}
|
|
function saveState(s: IntakeState) {
|
|
sessionStorage.setItem(storageKey, JSON.stringify(s));
|
|
}
|
|
|
|
// Expose helper on window for step components to read/write.
|
|
(window as any).PWIntake = {
|
|
get: (): IntakeState => loadState(),
|
|
set: (patch: Partial<IntakeState>) => {
|
|
const s = { ...loadState(), ...patch };
|
|
saveState(s);
|
|
return s;
|
|
},
|
|
patchIntakeData: (patch: Record<string, any>) => {
|
|
const s = loadState();
|
|
s.intake_data = { ...s.intake_data, ...patch };
|
|
saveState(s);
|
|
// If the patch touched line_105_categories, reshape the step list
|
|
if ("line_105_categories" in patch) {
|
|
const newSteps = reshapeSteps(patch.line_105_categories || []);
|
|
if (JSON.stringify(newSteps) !== JSON.stringify(steps)) {
|
|
steps = newSteps;
|
|
(window as any).PWIntake.steps = steps;
|
|
rebuildStepBar();
|
|
}
|
|
}
|
|
return s;
|
|
},
|
|
slug,
|
|
get steps() { return steps; },
|
|
set steps(s: string[]) { steps = s; },
|
|
};
|
|
|
|
// ── Pre-fill from order data when accessed via token (paid batch order) ──
|
|
// Also remove the "payment" step since payment is already done.
|
|
const urlParams = new URLSearchParams(window.location.search);
|
|
if (urlParams.has("token") || urlParams.has("order")) {
|
|
const payIdx = steps.indexOf("payment");
|
|
if (payIdx >= 0) {
|
|
steps.splice(payIdx, 1);
|
|
rebuildStepBar();
|
|
}
|
|
}
|
|
|
|
(async () => {
|
|
const params = urlParams;
|
|
const token = params.get("token");
|
|
const frn = params.get("frn");
|
|
const orderParam = params.get("order");
|
|
if (!token && !orderParam) return;
|
|
|
|
const API = (window as any).__PW_API || "";
|
|
try {
|
|
// Pre-fill from ?order=CO-xxx (e.g. from confirmation email link)
|
|
if (!token && orderParam) {
|
|
const state = loadState();
|
|
try {
|
|
const r = await fetch(`${API}/api/v1/compliance-orders/${orderParam}`);
|
|
if (r.ok) {
|
|
const data = await r.json();
|
|
const order = data.orders ? data.orders[0] : data;
|
|
if (order.customer_name && !state.name) state.name = order.customer_name;
|
|
if (order.customer_email && !state.email) state.email = order.customer_email;
|
|
const orderNum = order.order_number || orderParam;
|
|
state.order_number = orderNum;
|
|
state.intake_data = { ...state.intake_data, order_number: orderNum };
|
|
if (order.intake_data) {
|
|
const intake = typeof order.intake_data === "string" ? JSON.parse(order.intake_data) : order.intake_data;
|
|
state.intake_data = { ...state.intake_data, ...intake };
|
|
if (intake.frn && !state.entity?.frn) {
|
|
state.entity = { ...state.entity, frn: intake.frn };
|
|
}
|
|
}
|
|
saveState(state);
|
|
}
|
|
} catch {}
|
|
renderStep(state.step_index || 0);
|
|
return;
|
|
}
|
|
// Decode the token to get order_id + email — no server call needed for basic info
|
|
// The token is a JWT with {order_id, order_type, email}
|
|
const payload = JSON.parse(atob(token.split(".")[1]));
|
|
const state = loadState();
|
|
if (payload.email && !state.email) {
|
|
state.email = payload.email;
|
|
}
|
|
if (payload.order_id) {
|
|
state.intake_data = { ...state.intake_data, order_number: payload.order_id };
|
|
}
|
|
|
|
// Fetch customer info from the compliance order
|
|
if (payload.order_id) {
|
|
try {
|
|
const r = await fetch(`${API}/api/v1/compliance-orders/${payload.order_id}`, {
|
|
headers: { "Authorization": `Bearer ${token}` },
|
|
});
|
|
if (r.ok) {
|
|
const order = await r.json();
|
|
if (order.customer_name && !state.name) state.name = order.customer_name;
|
|
if (order.customer_email && !state.email) state.email = order.customer_email;
|
|
if (order.intake_data) {
|
|
state.intake_data = { ...state.intake_data, ...order.intake_data };
|
|
}
|
|
|
|
// If intake was already completed and user isn't revising, show "completed" screen
|
|
if (order.intake_data_validated && !urlParams.has("revise")) {
|
|
const body = document.querySelector(".pw-wizard-body") as HTMLElement;
|
|
const footer = document.querySelector(".pw-wizard-nav") as HTMLElement;
|
|
body.innerHTML = `
|
|
<div style="text-align:center;padding:2.5rem 1rem;">
|
|
<div style="width:64px;height:64px;margin:0 auto 1rem;background:#dcfce7;border-radius:50%;display:flex;align-items:center;justify-content:center;">
|
|
<svg style="width:32px;height:32px;color:#16a34a" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2.5"><path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7"/></svg>
|
|
</div>
|
|
<h2 style="font-size:1.25rem;font-weight:700;color:#111827;margin-bottom:0.5rem;">Intake Already Completed</h2>
|
|
<p style="color:#6b7280;font-size:0.92rem;margin-bottom:1.5rem;">You've already submitted your information for this order. We're processing your filing now.</p>
|
|
<div style="display:flex;gap:0.75rem;justify-content:center;flex-wrap:wrap;">
|
|
<a href="https://portal.performancewest.net" style="display:inline-block;background:#1a2744;color:#fff;padding:10px 24px;border-radius:8px;text-decoration:none;font-weight:600;font-size:0.9rem;">Go to Portal</a>
|
|
<button type="button" id="pw-revise-btn" style="display:inline-block;background:#fff;color:#1a2744;padding:10px 24px;border-radius:8px;font-weight:600;font-size:0.9rem;border:2px solid #e5e7eb;cursor:pointer;">Revise My Information</button>
|
|
</div>
|
|
</div>
|
|
`;
|
|
if (footer) footer.hidden = true;
|
|
document.getElementById("pw-revise-btn")?.addEventListener("click", () => {
|
|
sessionStorage.removeItem("pw-intake-" + slug);
|
|
const url = new URL(window.location.href);
|
|
url.searchParams.set("revise", "1");
|
|
window.location.href = url.toString();
|
|
});
|
|
return; // Don't render steps
|
|
}
|
|
}
|
|
} catch {}
|
|
}
|
|
|
|
saveState(state);
|
|
// Re-render the first step now that state is populated
|
|
renderStep(loadState().step_index || 0);
|
|
} catch {}
|
|
})();
|
|
|
|
function renderStep(idx: number) {
|
|
const state = loadState();
|
|
state.step_index = idx;
|
|
saveState(state);
|
|
wizard.querySelectorAll<HTMLElement>(".pw-wizard-body > [data-step]").forEach((el) => {
|
|
el.hidden = el.getAttribute("data-step") !== steps[idx];
|
|
});
|
|
wizard.querySelectorAll<HTMLElement>(".pw-step-chip").forEach((chip, i) => {
|
|
chip.setAttribute("data-active", String(i === idx));
|
|
chip.setAttribute("data-done", String(i < idx));
|
|
});
|
|
(document.getElementById("pw-back") as HTMLButtonElement).disabled = idx === 0;
|
|
const nextBtn = document.getElementById("pw-next") as HTMLButtonElement;
|
|
nextBtn.textContent = idx === steps.length - 1 ? "Finish" : "Next →";
|
|
// Scroll to page title (above the wizard)
|
|
const pageTitle = document.querySelector("main h1, .pw-order-intro h1");
|
|
(pageTitle || wizard).scrollIntoView({ behavior: "smooth", block: "start" });
|
|
|
|
// Let the active step hydrate itself
|
|
window.dispatchEvent(new CustomEvent("pw:step-shown", {
|
|
detail: { step: steps[idx], idx },
|
|
}));
|
|
|
|
// Show "review this info" notice on pre-filled steps (when accessed via token)
|
|
if (urlParams.has("token") || urlParams.has("frn")) {
|
|
const activeStep = wizard.querySelector<HTMLElement>(`.pw-wizard-body > [data-step="${steps[idx]}"]`);
|
|
if (activeStep && !activeStep.querySelector(".pw-prefill-notice")) {
|
|
const notice = document.createElement("div");
|
|
notice.className = "pw-prefill-notice";
|
|
notice.innerHTML = "We've pre-filled this from public sources. Please review carefully and correct any information that is inaccurate or outdated.";
|
|
activeStep.prepend(notice);
|
|
}
|
|
}
|
|
}
|
|
|
|
document.getElementById("pw-back")!.addEventListener("click", () => {
|
|
const s = loadState();
|
|
if (s.step_index > 0) renderStep(s.step_index - 1);
|
|
});
|
|
document.getElementById("pw-next")!.addEventListener("click", () => {
|
|
const s = loadState();
|
|
// Validate the current step — it may cancel by setting a flag.
|
|
const cancelEvt = new CustomEvent<{ cancel: boolean; reason?: string }>(
|
|
"pw:step-next", { detail: { cancel: false }, cancelable: true },
|
|
);
|
|
window.dispatchEvent(cancelEvt);
|
|
// Step components call evt.preventDefault() to block.
|
|
if (cancelEvt.defaultPrevented) return;
|
|
if (s.step_index < steps.length - 1) {
|
|
renderStep(s.step_index + 1);
|
|
} else {
|
|
// Last step — submit the intake data
|
|
submitIntake(s);
|
|
}
|
|
});
|
|
|
|
async function submitIntake(state: IntakeState) {
|
|
const nextBtn = document.getElementById("pw-next") as HTMLButtonElement;
|
|
nextBtn.disabled = true;
|
|
nextBtn.textContent = "Submitting...";
|
|
|
|
const params = new URLSearchParams(window.location.search);
|
|
const token = params.get("token");
|
|
const orderNumber = state.intake_data?.order_number || "";
|
|
const API = (window as any).__PW_API || "";
|
|
|
|
if (!orderNumber && !token) {
|
|
// No order context — this is a standalone order, go to payment
|
|
// (shouldn't happen since payment step was removed for token orders)
|
|
nextBtn.disabled = false;
|
|
nextBtn.textContent = "Finish";
|
|
return;
|
|
}
|
|
|
|
try {
|
|
// Save intake data to the compliance order
|
|
const saveResp = await fetch(`${API}/api/v1/compliance-orders/${orderNumber}/intake`, {
|
|
method: "PUT",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
...(token ? { "Authorization": `Bearer ${token}` } : {}),
|
|
},
|
|
body: JSON.stringify({
|
|
intake_data: state.intake_data,
|
|
entity: state.entity,
|
|
officers: state.officers,
|
|
}),
|
|
});
|
|
|
|
if (saveResp.ok) {
|
|
// Clear session storage
|
|
sessionStorage.removeItem(`pw-intake-${slug}`);
|
|
// Show success
|
|
const body = document.querySelector(".pw-wizard-body") as HTMLElement;
|
|
body.innerHTML = `
|
|
<div style="text-align:center;padding:3rem 1rem;">
|
|
<div style="width:64px;height:64px;margin:0 auto 1rem;background:#dcfce7;border-radius:50%;display:flex;align-items:center;justify-content:center;">
|
|
<svg style="width:32px;height:32px;color:#16a34a" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2.5"><path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7"/></svg>
|
|
</div>
|
|
<h2 style="font-size:1.3rem;font-weight:700;color:#111827;margin-bottom:0.5rem;">Intake Complete</h2>
|
|
<p style="color:#6b7280;font-size:0.95rem;margin-bottom:1.5rem;">Your information has been submitted. We'll begin processing your filing and email you with updates.</p>
|
|
<a href="https://portal.performancewest.net" style="display:inline-block;background:#1a2744;color:#fff;padding:10px 24px;border-radius:8px;text-decoration:none;font-weight:600;">Go to Client Portal</a>
|
|
</div>
|
|
`;
|
|
document.querySelector(".pw-wizard-stepbar")?.remove();
|
|
document.querySelector(".pw-wizard-footer")?.remove();
|
|
} else {
|
|
const err = await saveResp.json().catch(() => ({}));
|
|
alert(err.error || "Failed to submit. Please try again.");
|
|
nextBtn.disabled = false;
|
|
nextBtn.textContent = "Finish";
|
|
}
|
|
} catch (e) {
|
|
alert("Network error. Please try again.");
|
|
nextBtn.disabled = false;
|
|
nextBtn.textContent = "Finish";
|
|
}
|
|
}
|
|
|
|
// Kick off
|
|
renderStep(loadState().step_index || 0);
|
|
</script>
|