Add corporate record suggestions on Officer step, search all states
- OfficerStep searches entity_cache for matching corporations when loaded - Shows clickable suggestions with RA name and address to auto-fill Officer 1 - Pre-fills contact_name/email/phone from entity data (helps data-only filers) - Corp search endpoint: state param now optional (searches all states) - Corp search returns registered_agent and principal_address fields Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
ab7a2d7dc0
commit
159a576157
2 changed files with 103 additions and 18 deletions
|
|
@ -246,24 +246,35 @@ router.get("/api/v1/corp/search", async (req: Request, res: Response) => {
|
|||
res.status(400).json({ error: "q parameter required (min 2 chars)" });
|
||||
return;
|
||||
}
|
||||
if (!state || state.length !== 2) {
|
||||
res.status(400).json({ error: "state parameter required (2-letter code)" });
|
||||
return;
|
||||
}
|
||||
|
||||
const limit = Math.min(Number(req.query.limit) || 10, 20);
|
||||
|
||||
try {
|
||||
const { rows } = await pool.query(
|
||||
`SELECT entity_name, entity_number, entity_type, status, formation_date,
|
||||
similarity(entity_name, $2) AS sim
|
||||
FROM entity_cache
|
||||
WHERE state = $1 AND entity_name % $2
|
||||
ORDER BY sim DESC
|
||||
LIMIT 10`,
|
||||
[state, q],
|
||||
);
|
||||
// State is optional — if omitted, search all states
|
||||
const { rows } = state
|
||||
? await pool.query(
|
||||
`SELECT entity_name, entity_number, entity_type, status, formation_date,
|
||||
registered_agent, principal_address, state,
|
||||
similarity(entity_name, $2) AS sim
|
||||
FROM entity_cache
|
||||
WHERE state = $1 AND entity_name % $2
|
||||
ORDER BY sim DESC
|
||||
LIMIT $3`,
|
||||
[state, q, limit],
|
||||
)
|
||||
: await pool.query(
|
||||
`SELECT entity_name, entity_number, entity_type, status, formation_date,
|
||||
registered_agent, principal_address, state,
|
||||
similarity(entity_name, $2) AS sim
|
||||
FROM entity_cache
|
||||
WHERE entity_name % $1
|
||||
ORDER BY sim DESC
|
||||
LIMIT $2`,
|
||||
[q, limit],
|
||||
);
|
||||
|
||||
res.json({
|
||||
state,
|
||||
state: state || "all",
|
||||
query: q,
|
||||
count: rows.length,
|
||||
results: rows.map((r: any) => ({
|
||||
|
|
@ -274,11 +285,14 @@ router.get("/api/v1/corp/search", async (req: Request, res: Response) => {
|
|||
formation_date: r.formation_date
|
||||
? new Date(r.formation_date).toISOString().slice(0, 10)
|
||||
: null,
|
||||
registered_agent: r.registered_agent || null,
|
||||
principal_address: r.principal_address || null,
|
||||
state: r.state || null,
|
||||
})),
|
||||
});
|
||||
} catch (err: any) {
|
||||
if (err?.code === "42P01") {
|
||||
res.json({ state, query: q, count: 0, results: [], note: "Entity database not yet populated for this state." });
|
||||
res.json({ state: state || "all", query: q, count: 0, results: [], note: "Entity database not yet populated for this state." });
|
||||
} else {
|
||||
console.error("[corp/search] Error:", err);
|
||||
res.status(500).json({ error: "Search failed" });
|
||||
|
|
|
|||
|
|
@ -15,6 +15,12 @@
|
|||
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>
|
||||
|
||||
<label class="pw-field">How many officers will you list?</label>
|
||||
<select id="pw-officer-count" class="pw-input">
|
||||
<option value="1">1 — sole proprietor / single officer</option>
|
||||
|
|
@ -83,6 +89,12 @@
|
|||
.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>
|
||||
|
||||
|
|
@ -113,6 +125,60 @@
|
|||
};
|
||||
}
|
||||
|
||||
// Fetch matching corporate records for officer suggestions
|
||||
async function loadOfficerSuggestions(entityName: string) {
|
||||
if (!entityName) return;
|
||||
const API = (window as any).__PW_API || "";
|
||||
const suggestDiv = g<HTMLElement>("pw-officer-suggestions");
|
||||
const listDiv = g<HTMLElement>("pw-officer-suggest-list");
|
||||
|
||||
try {
|
||||
// Search entity_cache for matching corporations across all states
|
||||
const resp = await fetch(`${API}/api/v1/corp/search?q=${encodeURIComponent(entityName)}&limit=5`);
|
||||
if (!resp.ok) return;
|
||||
const data = await resp.json();
|
||||
const results = data.results || [];
|
||||
|
||||
if (results.length === 0) return;
|
||||
|
||||
listDiv.innerHTML = "";
|
||||
for (const r of results) {
|
||||
const btn = document.createElement("button");
|
||||
btn.type = "button";
|
||||
btn.className = "pw-suggest-btn";
|
||||
const ra = r.registered_agent || "";
|
||||
const addr = r.principal_address || "";
|
||||
btn.innerHTML = `
|
||||
<span class="sg-name">${r.entity_name}</span>
|
||||
<span class="sg-detail">${r.state || ""} · ${r.entity_type || ""} · ${r.status || ""}</span>
|
||||
${ra ? `<span class="sg-detail">RA: ${ra}</span>` : ""}
|
||||
${addr ? `<span class="sg-detail">${addr}</span>` : ""}
|
||||
`;
|
||||
btn.addEventListener("click", () => {
|
||||
// Auto-fill Officer 1 from this record
|
||||
const f = officerFields(1);
|
||||
if (ra && !f.n.value.trim()) {
|
||||
f.n.value = ra;
|
||||
}
|
||||
// Parse address if available
|
||||
if (addr) {
|
||||
const parts = addr.split(",").map((s: string) => s.trim());
|
||||
if (parts.length >= 1 && !f.street.value.trim()) f.street.value = parts[0];
|
||||
if (parts.length >= 2 && !f.city.value.trim()) f.city.value = parts[parts.length - 2] || parts[1];
|
||||
if (parts.length >= 3) {
|
||||
const stateZip = parts[parts.length - 1].trim().split(/\s+/);
|
||||
if (stateZip[0] && !f.state.value.trim()) f.state.value = stateZip[0];
|
||||
if (stateZip[1] && !f.zip.value.trim()) f.zip.value = stateZip[1];
|
||||
}
|
||||
}
|
||||
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();
|
||||
|
|
@ -125,15 +191,20 @@
|
|||
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 || "") : "");
|
||||
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 || "";
|
||||
if (f.p) f.p.value = oc.phone || "";
|
||||
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 || "") : "");
|
||||
}
|
||||
|
||||
// Load corporate record suggestions if officer 1 name is empty
|
||||
if (!o[0]?.name && s.entity?.legal_name) {
|
||||
loadOfficerSuggestions(s.entity.legal_name);
|
||||
}
|
||||
});
|
||||
|
||||
window.addEventListener("pw:step-next", (evt: any) => {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue