new-site/site/src/components/intake/steps/ForeignQualStep.astro
justin f8cd37ac8c Initial commit — Performance West telecom compliance platform
Includes: API (Express/TypeScript), Astro site, Python workers,
document generators, FCC compliance tools, Canada CRTC formation,
Ansible infrastructure, and deployment scripts.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-27 06:54:22 -05:00

241 lines
9.7 KiB
Text

---
// ForeignQualStep — multi-state COA picker with per-state fee preview.
// Calls GET /api/v1/foreign-qualification/jurisdictions to load the
// state catalog, then POST /quote to update totals in real-time.
---
<div class="pw-step">
<h2>Select target states for foreign qualification</h2>
<p class="pw-help">
Choose every state where your entity needs authorization to do business.
Each state charges its own Certificate of Authority fee. We also provision
a registered agent in each target state ($125/yr each).
</p>
<div class="pw-fq-controls">
<label class="pw-fq-et">
Entity type:
<select id="pw-fq-entity-type">
<option value="llc">LLC</option>
<option value="corporation">Corporation</option>
<option value="s_corp">S-Corp</option>
</select>
</label>
<label class="pw-fq-home">
Home state (where formed):
<input type="text" id="pw-fq-home-state" maxlength="2" placeholder="WY" style="width:50px;text-transform:uppercase" />
</label>
<label>
<input type="checkbox" id="pw-fq-ra" checked /> Include registered agent ($125/yr each)
</label>
<label>
<input type="checkbox" id="pw-fq-expedited" /> Expedited processing
</label>
</div>
<div class="pw-fq-actions">
<button type="button" id="pw-fq-select-all" class="pw-btn-plain pw-btn">Select all</button>
<button type="button" id="pw-fq-clear" class="pw-btn-plain pw-btn">Clear</button>
<span id="pw-fq-count">0 states selected</span>
</div>
<div id="pw-fq-grid" class="pw-fq-grid"></div>
<div id="pw-fq-quote" class="pw-fq-quote" hidden>
<table id="pw-fq-quote-table">
<thead>
<tr>
<th>State</th>
<th>State fee</th>
<th>RA</th>
<th>Service</th>
<th>Total</th>
</tr>
</thead>
<tbody id="pw-fq-quote-body"></tbody>
<tfoot>
<tr>
<th colspan="4" style="text-align:right">Grand total:</th>
<th id="pw-fq-grand-total">$0</th>
</tr>
</tfoot>
</table>
</div>
<div id="pw-fq-err" class="pw-err" hidden></div>
</div>
<style>
.pw-step h2 { margin: 0 0 0.5rem; color: #1a2744; }
.pw-help { color: #64748b; font-size: 0.9rem; margin-bottom: 1rem; }
.pw-fq-controls { display: flex; flex-wrap: wrap; gap: 1rem; align-items: center; margin-bottom: 1rem; }
.pw-fq-controls label { font-size: 0.85rem; color: #475569; display: flex; align-items: center; gap: 0.35rem; }
.pw-fq-controls select, .pw-fq-controls input[type="text"] { padding: 0.3rem 0.5rem; border: 1px solid #cbd5e1; border-radius: 4px; }
.pw-fq-actions { display: flex; gap: 0.5rem; align-items: center; margin-bottom: 0.75rem; }
.pw-btn { padding: 0.4rem 0.9rem; border: 0; border-radius: 6px; font-size: 0.85rem; cursor: pointer; }
.pw-btn-plain { background: #e2e8f0; color: #1f2937; }
#pw-fq-count { color: #475569; font-size: 0.85rem; margin-left: 0.5rem; }
.pw-fq-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); gap: 0.5rem; }
.pw-fq-grid label { display: flex; align-items: center; gap: 0.35rem; font-size: 0.85rem;
padding: 0.4rem 0.6rem; border: 1px solid #e2e8f0; border-radius: 6px; cursor: pointer; }
.pw-fq-grid label:has(input:checked) { background: #e0f2fe; border-color: #38bdf8; }
.pw-fq-grid .pw-fq-fee { color: #64748b; font-size: 0.75rem; margin-left: auto; }
.pw-fq-quote { margin-top: 1rem; }
.pw-fq-quote table { width: 100%; border-collapse: collapse; font-size: 0.85rem; }
.pw-fq-quote th, .pw-fq-quote td { padding: 0.35rem 0.5rem; border-bottom: 1px solid #e2e8f0; text-align: right; }
.pw-fq-quote th:first-child, .pw-fq-quote td:first-child { text-align: left; }
.pw-fq-quote thead th { background: #f8fafc; font-size: 0.75rem; text-transform: uppercase; color: #475569; }
.pw-fq-quote tfoot th { background: #f1f5f9; font-size: 0.95rem; }
.pw-err { color: #b91c1c; margin-top: 0.75rem; font-size: 0.9rem; }
</style>
<script>
const grid = document.getElementById("pw-fq-grid")!;
const countEl = document.getElementById("pw-fq-count")!;
const quoteDiv = document.getElementById("pw-fq-quote") as HTMLElement;
const quoteBody = document.getElementById("pw-fq-quote-body")!;
const grandEl = document.getElementById("pw-fq-grand-total")!;
const err = document.getElementById("pw-fq-err") as HTMLDivElement;
const entityTypeEl = document.getElementById("pw-fq-entity-type") as HTMLSelectElement;
const homeStateEl = document.getElementById("pw-fq-home-state") as HTMLInputElement;
const raEl = document.getElementById("pw-fq-ra") as HTMLInputElement;
const expEl = document.getElementById("pw-fq-expedited") as HTMLInputElement;
interface JurisdictionRow {
code: string; name: string; foreign_llc_fee: number | null;
foreign_corp_fee: number | null; publication_required: boolean;
typical_processing_days: number | null;
}
let jurisdictions: JurisdictionRow[] = [];
async function loadJurisdictions() {
try {
const r = await fetch("/api/v1/foreign-qualification/jurisdictions");
if (!r.ok) throw new Error(`HTTP ${r.status}`);
const d = await r.json();
jurisdictions = d.jurisdictions || [];
renderGrid();
} catch (e) {
err.hidden = false;
err.textContent = `Failed to load jurisdictions: ${(e as Error).message}`;
}
}
function renderGrid() {
grid.innerHTML = "";
for (const j of jurisdictions) {
const et = entityTypeEl.value;
const fee = et === "llc" || et === "pllc" ? j.foreign_llc_fee : j.foreign_corp_fee;
const lbl = document.createElement("label");
lbl.innerHTML =
`<input type="checkbox" data-code="${j.code}" />` +
`<span>${j.code} — ${j.name}</span>` +
`<span class="pw-fq-fee">${fee != null ? "$" + (fee / 100).toFixed(0) : "—"}</span>`;
grid.appendChild(lbl);
}
grid.querySelectorAll("input").forEach(cb => cb.addEventListener("change", updateCount));
}
function getSelected(): string[] {
return Array.from(grid.querySelectorAll<HTMLInputElement>("input:checked"))
.map(cb => cb.dataset.code!);
}
function updateCount() {
const sel = getSelected();
countEl.textContent = `${sel.length} state${sel.length === 1 ? "" : "s"} selected`;
if (sel.length > 0) fetchQuote(sel);
else { quoteDiv.hidden = true; }
}
async function fetchQuote(states: string[]) {
try {
const r = await fetch("/api/v1/foreign-qualification/quote", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
home_state_code: homeStateEl.value.trim().toUpperCase() || "WY",
entity_type: entityTypeEl.value,
target_states: states,
include_ra_each: raEl.checked,
expedited: expEl.checked,
}),
});
if (!r.ok) throw new Error(`HTTP ${r.status}`);
const d = await r.json();
renderQuote(d);
} catch (e) {
err.hidden = false;
err.textContent = `Quote failed: ${(e as Error).message}`;
}
}
function renderQuote(d: any) {
quoteBody.innerHTML = "";
for (const it of (d.items || [])) {
if (it.error) continue;
const tr = document.createElement("tr");
tr.innerHTML =
`<td>${it.state_code} — ${it.state_name}</td>` +
`<td>$${(it.state_fee_cents / 100).toFixed(0)}</td>` +
`<td>$${(it.nwra_ra_fee_cents / 100).toFixed(0)}</td>` +
`<td>$${(it.service_fee_cents / 100).toFixed(0)}</td>` +
`<td><strong>$${(it.total_cents / 100).toFixed(2)}</strong></td>`;
quoteBody.appendChild(tr);
}
grandEl.textContent = `$${(d.grand_total_cents / 100).toLocaleString("en-US", { minimumFractionDigits: 2 })}`;
quoteDiv.hidden = false;
err.hidden = true;
}
document.getElementById("pw-fq-select-all")!.addEventListener("click", () => {
grid.querySelectorAll<HTMLInputElement>("input").forEach(cb => cb.checked = true);
updateCount();
});
document.getElementById("pw-fq-clear")!.addEventListener("click", () => {
grid.querySelectorAll<HTMLInputElement>("input").forEach(cb => cb.checked = false);
updateCount();
});
entityTypeEl.addEventListener("change", () => { renderGrid(); updateCount(); });
raEl.addEventListener("change", () => { const s = getSelected(); if (s.length) fetchQuote(s); });
expEl.addEventListener("change", () => { const s = getSelected(); if (s.length) fetchQuote(s); });
// Prefill from wizard state.
window.addEventListener("pw:step-shown", (evt: any) => {
if (evt.detail.step !== "foreign_qual") return;
const s = (window as any).PWIntake.get();
if (s.entity?.address_state) homeStateEl.value = s.entity.address_state;
if (s.entity?.entity_type) entityTypeEl.value = s.entity.entity_type;
if (s.intake_data?.target_states) {
const prev = s.intake_data.target_states as string[];
grid.querySelectorAll<HTMLInputElement>("input").forEach(cb => {
if (prev.includes(cb.dataset.code!)) cb.checked = true;
});
updateCount();
}
});
// Persist into wizard state on Next.
window.addEventListener("pw:step-next", (evt: any) => {
const PW = (window as any).PWIntake;
if (PW.steps[PW.get().step_index] !== "foreign_qual") return;
const sel = getSelected();
if (sel.length === 0) {
err.hidden = false;
err.textContent = "Select at least one target state.";
evt.preventDefault();
return;
}
err.hidden = true;
PW.patchIntakeData({
home_state_code: homeStateEl.value.trim().toUpperCase(),
entity_type: entityTypeEl.value,
target_states: sel,
include_ra_each: raEl.checked,
expedited: expEl.checked,
});
});
loadJurisdictions();
</script>