new-site/site/src/components/intake/steps/OfficerStep.astro
justin acf63eb819 Officer suggestions: use FCC data (RMD contact, CORES address) instead of entity_cache
Entity cache has no RA/officer data yet. Instead, fetch the FCC lookup
(quick mode) and offer RMD contact name + address and CORES principal
address as clickable suggestions to auto-fill Officer 1.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-29 01:02:18 -05:00

290 lines
13 KiB
Text

---
// OfficerStep — CEO + 2nd/3rd officers with business addresses
// per 2026 FCC Form 499-A Block 2-C (Lines 219-226).
//
// Corporate/LLC: require 3 officers (or explain fewer). Sole proprietor:
// 1 is fine. Partnership: managing partner + 2 with greatest financial
// interest. Entity structure drives the required count.
---
<div class="pw-step">
<h2>Officers</h2>
<p class="pw-help">
Lines 219-226 of Form 499-A require name, title, and business address
for up to 3 officers. Entity structure drives how many we collect —
a sole proprietor gives 1; a corporation gives 3.
</p>
<!-- Suggested officers from corporate records -->
<div id="pw-officer-suggestions" class="pw-suggestions" hidden>
<p class="pw-suggest-label">We found corporate records that may match. Select to auto-fill:</p>
<div id="pw-officer-suggest-list"></div>
</div>
<p class="pw-help" style="margin-top:0;font-size:0.82rem;color:#94a3b8;">
Fill in Officer 1 (required). Officers 2 and 3 are optional — leave blank if not applicable.
</p>
<select id="pw-officer-count" class="pw-input" hidden>
<option value="3" selected>3</option>
</select>
<fieldset class="pw-fieldset">
<legend>Officer 1 — CEO / highest-ranking</legend>
<div class="pw-row">
<div><label class="pw-field">Name</label><input type="text" id="pw-o1-n" class="pw-input" required /></div>
<div><label class="pw-field">Title</label><input type="text" id="pw-o1-t" class="pw-input" value="Chief Executive Officer" /></div>
</div>
<div class="pw-row">
<div><label class="pw-field">Email</label><input type="email" id="pw-o1-e" class="pw-input" required /></div>
<div><label class="pw-field">Phone</label><input type="tel" id="pw-o1-p" class="pw-input" /></div>
</div>
<label class="pw-field">Business street</label>
<input type="text" id="pw-o1-street" class="pw-input" />
<div class="pw-row">
<div><label class="pw-field">City</label><input type="text" id="pw-o1-city" class="pw-input" /></div>
<div><label class="pw-field">State</label><input type="text" id="pw-o1-state" class="pw-input" maxlength="2" /></div>
<div><label class="pw-field">ZIP</label><input type="text" id="pw-o1-zip" class="pw-input" maxlength="10" /></div>
</div>
</fieldset>
<fieldset class="pw-fieldset" id="pw-o2-wrap">
<legend>Officer 2 <span style="font-weight:400;color:#94a3b8;font-size:0.82rem;">(optional)</span></legend>
<div class="pw-row">
<div><label class="pw-field">Name</label><input type="text" id="pw-o2-n" class="pw-input" /></div>
<div><label class="pw-field">Title</label><input type="text" id="pw-o2-t" class="pw-input" /></div>
</div>
<label class="pw-field">Business street</label>
<input type="text" id="pw-o2-street" class="pw-input" />
<div class="pw-row">
<div><label class="pw-field">City</label><input type="text" id="pw-o2-city" class="pw-input" /></div>
<div><label class="pw-field">State</label><input type="text" id="pw-o2-state" class="pw-input" maxlength="2" /></div>
<div><label class="pw-field">ZIP</label><input type="text" id="pw-o2-zip" class="pw-input" maxlength="10" /></div>
</div>
</fieldset>
<fieldset class="pw-fieldset" id="pw-o3-wrap">
<legend>Officer 3 <span style="font-weight:400;color:#94a3b8;font-size:0.82rem;">(optional)</span></legend>
<div class="pw-row">
<div><label class="pw-field">Name</label><input type="text" id="pw-o3-n" class="pw-input" /></div>
<div><label class="pw-field">Title</label><input type="text" id="pw-o3-t" class="pw-input" /></div>
</div>
<label class="pw-field">Business street</label>
<input type="text" id="pw-o3-street" class="pw-input" />
<div class="pw-row">
<div><label class="pw-field">City</label><input type="text" id="pw-o3-city" class="pw-input" /></div>
<div><label class="pw-field">State</label><input type="text" id="pw-o3-state" class="pw-input" maxlength="2" /></div>
<div><label class="pw-field">ZIP</label><input type="text" id="pw-o3-zip" class="pw-input" maxlength="10" /></div>
</div>
</fieldset>
<div id="pw-officer-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-field { display: block; font-weight: 600; color: #1f2937; margin: 0.4rem 0 0.15rem; font-size: 0.85rem; }
.pw-input { width: 100%; padding: 0.5rem 0.7rem; border: 1px solid #cbd5e1; border-radius: 6px; font-size: 0.92rem; }
.pw-fieldset { border: 1px solid #e2e8f0; border-radius: 8px; padding: 0.75rem 1rem 1rem; margin: 1rem 0; }
.pw-fieldset legend { font-weight: 600; color: #1a2744; padding: 0 0.5rem; }
.pw-row { display: flex; gap: 0.75rem; flex-wrap: wrap; }
.pw-row > * { flex: 1 1 140px; }
.pw-suggestions { background: #eff6ff; border: 1px solid #bfdbfe; border-radius: 8px; padding: 0.75rem; margin-bottom: 1rem; }
.pw-suggest-label { font-size: 0.82rem; color: #1e40af; font-weight: 600; margin: 0 0 0.5rem; }
.pw-suggest-btn { display: block; width: 100%; text-align: left; padding: 0.5rem 0.75rem; margin: 0.25rem 0; border: 1px solid #dbeafe; background: #fff; border-radius: 6px; cursor: pointer; font-size: 0.85rem; color: #1f2937; transition: all 0.1s; }
.pw-suggest-btn:hover { border-color: #3b82f6; background: #f0f4ff; }
.pw-suggest-btn .sg-name { font-weight: 600; }
.pw-suggest-btn .sg-detail { font-size: 0.78rem; color: #6b7280; }
.pw-err { color: #b91c1c; margin-top: 0.75rem; font-size: 0.9rem; }
</style>
<script>
const g = <T extends HTMLElement>(id: string) => document.getElementById(id) as T;
const count = g<HTMLSelectElement>("pw-officer-count");
const o2wrap = g<HTMLElement>("pw-o2-wrap");
const o3wrap = g<HTMLElement>("pw-o3-wrap");
const err = g<HTMLDivElement>("pw-officer-err");
// All 3 officer sections always visible — 2 & 3 are optional
function syncCount() {};
function officerFields(i: number) {
return {
n: g<HTMLInputElement>(`pw-o${i}-n`),
t: g<HTMLInputElement>(`pw-o${i}-t`),
e: i === 1 ? g<HTMLInputElement>("pw-o1-e") : null,
p: i === 1 ? g<HTMLInputElement>("pw-o1-p") : null,
street: g<HTMLInputElement>(`pw-o${i}-street`),
city: g<HTMLInputElement>(`pw-o${i}-city`),
state: g<HTMLInputElement>(`pw-o${i}-state`),
zip: g<HTMLInputElement>(`pw-o${i}-zip`),
};
}
// Auto-fill Officer 1 from FCC data if available but not yet filled
async function prefillFromFCC(entity: any) {
if (!entity?.frn) return;
const API = (window as any).__PW_API || "";
const suggestDiv = g<HTMLElement>("pw-officer-suggestions");
const listDiv = g<HTMLElement>("pw-officer-suggest-list");
const f = officerFields(1);
// If Officer 1 already has a name, don't overwrite
if (f.n.value.trim()) return;
try {
const resp = await fetch(`${API}/api/v1/fcc/lookup?frn=${entity.frn}&quick=1`);
if (!resp.ok) return;
const d = await resp.json();
const suggestions: Array<{name: string; title: string; source: string; address?: string; city?: string; state?: string; zip?: string}> = [];
// RMD contact
if (d.rmd?.contact_name) {
const addrLines = (d.rmd.business_address || "").split("\n").map((s: string) => s.trim()).filter(Boolean);
const lastLine = addrLines[addrLines.length - 1] || "";
const stateZipMatch = lastLine.match(/([A-Z]{2})\s+(\d{5})/);
suggestions.push({
name: d.rmd.contact_name,
title: "Contact (from RMD filing)",
source: "FCC RMD",
address: addrLines.length > 1 ? addrLines.slice(0, -1).join(", ") : addrLines[0],
city: stateZipMatch ? lastLine.replace(stateZipMatch[0], "").replace(/,?\s*$/, "").trim() : "",
state: stateZipMatch ? stateZipMatch[1] : "",
zip: stateZipMatch ? stateZipMatch[2] : "",
});
}
// CORES address (entity-level, not a person name)
if (d.cores?.address && d.cores.city) {
suggestions.push({
name: "",
title: "Principal address (from CORES)",
source: "FCC CORES",
address: d.cores.address,
city: d.cores.city,
state: d.cores.state || "",
zip: d.cores.zip || "",
});
}
if (suggestions.length === 0) return;
listDiv.innerHTML = "";
for (const s of suggestions) {
const btn = document.createElement("button");
btn.type = "button";
btn.className = "pw-suggest-btn";
btn.innerHTML = `
${s.name ? `<span class="sg-name">${s.name}</span>` : ""}
<span class="sg-detail">${s.title}</span>
${s.address ? `<span class="sg-detail">${[s.address, s.city, s.state, s.zip].filter(Boolean).join(", ")}</span>` : ""}
`;
btn.addEventListener("click", () => {
if (s.name) f.n.value = s.name;
if (s.address) f.street.value = s.address;
if (s.city) f.city.value = s.city;
if (s.state) f.state.value = s.state;
if (s.zip) f.zip.value = s.zip;
suggestDiv.hidden = true;
});
listDiv.appendChild(btn);
}
suggestDiv.hidden = false;
} catch {}
}
window.addEventListener("pw:step-shown", (evt: any) => {
if (evt.detail.step !== "officer") return;
const s = (window as any).PWIntake.get();
const o = s.officers || [{}, {}, {}];
const structure = s.entity?.entity_structure;
const existingCount = s.entity?.officer_count_claimed;
count.value = String(existingCount || (structure === "sole_prop" ? 1 : structure === "partnership" ? 3 : 3));
syncCount();
for (let i = 1; i <= 3; i++) {
const f = officerFields(i);
const oc = o[i - 1] || {};
f.n.value = oc.name || (i === 1 ? (s.entity?.ceo_name || s.entity?.contact_name || "") : "");
f.t.value = oc.title || (i === 1 ? (s.entity?.ceo_title || "Chief Executive Officer") : "");
if (f.e) f.e.value = oc.email || (i === 1 ? (s.entity?.contact_email || "") : "");
if (f.p) f.p.value = oc.phone || (i === 1 ? (s.entity?.contact_phone || "") : "");
f.street.value = oc.street || (i === 1 ? (s.entity?.address_street || "") : "");
f.city.value = oc.city || (i === 1 ? (s.entity?.address_city || "") : "");
f.state.value = oc.state || (i === 1 ? (s.entity?.address_state || "") : "");
f.zip.value = oc.zip || (i === 1 ? (s.entity?.address_zip || "") : "");
}
// Show FCC-sourced suggestions if Officer 1 is empty
if (s.entity?.frn) {
prefillFromFCC(s.entity);
}
});
window.addEventListener("pw:step-next", (evt: any) => {
const PW = (window as any).PWIntake;
if (PW.steps[PW.get().step_index] !== "officer") return;
const officers: any[] = [];
const missing: string[] = [];
for (let i = 1; i <= 3; i++) {
const f = officerFields(i);
if (i === 1) {
// Officer 1 is required
if (!f.n.value.trim()) missing.push("Officer 1 name");
if (!f.t.value.trim()) missing.push("Officer 1 title");
if (!f.street.value.trim()) missing.push("Officer 1 street address");
if (!f.city.value.trim()) missing.push("Officer 1 city");
}
// Only include officer if name is filled (skip blank optional officers)
if (f.n.value.trim()) {
officers.push({
name: f.n.value.trim(), title: f.t.value.trim(),
email: f.e?.value.trim() || "", phone: f.p?.value.trim() || "",
street: f.street.value.trim(), city: f.city.value.trim(),
state: f.state.value.trim().toUpperCase(), zip: f.zip.value.trim(),
});
}
}
if (missing.length) {
err.hidden = false; err.textContent = `Required: ${missing.join(", ")}`;
evt.preventDefault(); return;
}
err.hidden = true;
const s = PW.get();
PW.set({
officers,
entity: {
...s.entity,
ceo_name: officers[0].name,
ceo_title: officers[0].title,
contact_name: officers[0].name,
contact_email: officers[0].email,
contact_phone: officers[0].phone,
officer_1_street: officers[0].street,
officer_1_city: officers[0].city,
officer_1_state: officers[0].state,
officer_1_zip: officers[0].zip,
officer_2_name: officers[1]?.name || null,
officer_2_title: officers[1]?.title || null,
officer_2_street: officers[1]?.street || null,
officer_2_city: officers[1]?.city || null,
officer_2_state: officers[1]?.state || null,
officer_2_zip: officers[1]?.zip || null,
officer_3_name: officers[2]?.name || null,
officer_3_title: officers[2]?.title || null,
officer_3_street: officers[2]?.street || null,
officer_3_city: officers[2]?.city || null,
officer_3_state: officers[2]?.state || null,
officer_3_zip: officers[2]?.zip || null,
officer_count_claimed: n,
},
});
PW.patchIntakeData({
officer: officers[0],
officer_2: officers[1] || null,
officer_3: officers[2] || null,
officer_count_claimed: n,
});
});
</script>