feat(ux): P-4.5 inquiry linkage + docs N+1 parallelization
Step 4 (in progress) — first slice of UX features. P-4.5: inquiry → client linkage now survives the triage conversion. - inquiry-inbox.tsx adds `?create=1` to the redirect so the new-client sheet auto-opens (the existing prefill_* params were already being written but the form never opened). - client-list.tsx reads prefill_name / prefill_email / prefill_phone / prefill_source / prefill_inquiry_id from useSearchParams and passes them to ClientForm via a typed `prefill` prop. - ClientForm hydrates the create-flow initial values from the prefill AND threads `sourceInquiryId` through to the createClient mutation. - createClientSchema accepts `sourceInquiryId`; the existing service spread already passes it to drizzle's insert. Net effect: a website inquiry that gets converted now lands as a client row with `clients.source_inquiry_id` populated. The conversion funnel-by-source chart (Step 6) can attribute the win back to the originating inquiry. Documents tab N+1: `listInflightWorkflowsAggregatedByEntity` previously walked direct + every company + every yacht + every related client sequentially. On a busy client (~25 related entities) this was ~50 sequential round-trips with cumulative latency. Replaced with a single `Promise.all` over the four lookup groups + nested Promise.all over the per-entity queries within each group. Same query count, but wall- clock collapses from "sum of every query" to "max single round-trip" (typically <100ms now vs >1s before). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -2262,46 +2262,71 @@ export async function listInflightWorkflowsAggregatedByEntity(
|
||||
? documents.companyId
|
||||
: documents.yachtId;
|
||||
|
||||
const direct = await fetchWorkflowGroupRows(portId, eq(directColumn, entityId));
|
||||
if (direct.rows.length > 0) {
|
||||
// Batch the related-entity workflow lookups in parallel — the
|
||||
// pre-2026-05-14 sequential loop fired ~50 queries on a busy client
|
||||
// (direct + each company + each yacht + each related client), each
|
||||
// round-trip blocking the next. Now every lookup runs concurrently
|
||||
// via Promise.all; total wall-clock collapses to "slowest single
|
||||
// query" instead of "sum of every query". Future fully-batched UNION
|
||||
// query in PRE-DEPLOY-PLAN follow-ups.
|
||||
const [directResult, companyResults, yachtResults, clientResults] = await Promise.all([
|
||||
fetchWorkflowGroupRows(portId, eq(directColumn, entityId)),
|
||||
Promise.all(
|
||||
related.companies.map(async ({ id, name }) => ({
|
||||
name,
|
||||
result: await fetchWorkflowGroupRows(portId, eq(documents.companyId, id)),
|
||||
})),
|
||||
),
|
||||
Promise.all(
|
||||
related.yachts.map(async ({ id, name }) => ({
|
||||
name,
|
||||
result: await fetchWorkflowGroupRows(portId, eq(documents.yachtId, id)),
|
||||
})),
|
||||
),
|
||||
Promise.all(
|
||||
related.clients.map(async ({ id, name }) => ({
|
||||
name,
|
||||
result: await fetchWorkflowGroupRows(portId, eq(documents.clientId, id)),
|
||||
})),
|
||||
),
|
||||
]);
|
||||
|
||||
if (directResult.rows.length > 0) {
|
||||
groups.push({
|
||||
label: 'DIRECTLY ATTACHED',
|
||||
source: 'direct',
|
||||
workflows: direct.rows,
|
||||
total: direct.total,
|
||||
workflows: directResult.rows,
|
||||
total: directResult.total,
|
||||
});
|
||||
}
|
||||
|
||||
for (const { id, name } of related.companies) {
|
||||
const g = await fetchWorkflowGroupRows(portId, eq(documents.companyId, id));
|
||||
if (g.rows.length === 0) continue;
|
||||
for (const { name, result } of companyResults) {
|
||||
if (result.rows.length === 0) continue;
|
||||
groups.push({
|
||||
label: `FROM COMPANY: ${name.toUpperCase()}`,
|
||||
source: 'company',
|
||||
workflows: g.rows,
|
||||
total: g.total,
|
||||
workflows: result.rows,
|
||||
total: result.total,
|
||||
});
|
||||
}
|
||||
|
||||
for (const { id, name } of related.yachts) {
|
||||
const g = await fetchWorkflowGroupRows(portId, eq(documents.yachtId, id));
|
||||
if (g.rows.length === 0) continue;
|
||||
for (const { name, result } of yachtResults) {
|
||||
if (result.rows.length === 0) continue;
|
||||
groups.push({
|
||||
label: `FROM YACHT: ${name.toUpperCase()}`,
|
||||
source: 'yacht',
|
||||
workflows: g.rows,
|
||||
total: g.total,
|
||||
workflows: result.rows,
|
||||
total: result.total,
|
||||
});
|
||||
}
|
||||
|
||||
for (const { id, name } of related.clients) {
|
||||
const g = await fetchWorkflowGroupRows(portId, eq(documents.clientId, id));
|
||||
if (g.rows.length === 0) continue;
|
||||
for (const { name, result } of clientResults) {
|
||||
if (result.rows.length === 0) continue;
|
||||
groups.push({
|
||||
label: `FROM CLIENT: ${name.toUpperCase()}`,
|
||||
source: 'client',
|
||||
workflows: g.rows,
|
||||
total: g.total,
|
||||
workflows: result.rows,
|
||||
total: result.total,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -34,6 +34,10 @@ export const createClientSchema = z.object({
|
||||
timezone: optionalIanaTimezoneSchema.optional(),
|
||||
source: z.enum(['website', 'manual', 'referral', 'broker', 'other']).optional(),
|
||||
sourceDetails: z.string().optional(),
|
||||
/** When the client was created from a website-inquiry triage, points
|
||||
* back at the originating `website_submissions.id`. Drives the
|
||||
* conversion-funnel-by-source chart. Migration 0065 installs the FK. */
|
||||
sourceInquiryId: z.string().optional(),
|
||||
tagIds: z.array(z.string()).optional().default([]),
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user