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:
@@ -40,6 +40,18 @@ interface ClientFormProps {
|
||||
* or opening the create-interest dialog pre-filled with that
|
||||
* clientId. Skipped in edit mode. */
|
||||
onUseExistingClient?: (clientId: string) => void;
|
||||
/** Optional initial values for the create flow — used by the
|
||||
* inquiry-inbox "Convert to client" triage step (P-4.5) so the rep
|
||||
* doesn't retype values they just read in the inbox. The
|
||||
* `sourceInquiryId` is persisted to `clients.source_inquiry_id` on
|
||||
* save, preserving the inquiry → client lineage for reporting. */
|
||||
prefill?: {
|
||||
fullName?: string;
|
||||
email?: string;
|
||||
phone?: string;
|
||||
source?: 'website' | 'manual' | 'referral' | 'broker' | 'other';
|
||||
sourceInquiryId?: string;
|
||||
};
|
||||
/** If provided, form is in edit mode */
|
||||
client?: {
|
||||
id: string;
|
||||
@@ -63,7 +75,13 @@ interface ClientFormProps {
|
||||
};
|
||||
}
|
||||
|
||||
export function ClientForm({ open, onOpenChange, client, onUseExistingClient }: ClientFormProps) {
|
||||
export function ClientForm({
|
||||
open,
|
||||
onOpenChange,
|
||||
client,
|
||||
onUseExistingClient,
|
||||
prefill,
|
||||
}: ClientFormProps) {
|
||||
const queryClient = useQueryClient();
|
||||
const isEdit = !!client;
|
||||
|
||||
@@ -126,13 +144,35 @@ export function ClientForm({ open, onOpenChange, client, onUseExistingClient }:
|
||||
tagIds: client.tags?.map((t) => t.id) ?? [],
|
||||
});
|
||||
} else if (!client && open) {
|
||||
// P-4.5: when the inquiry-inbox triage flow opens the form via
|
||||
// `?create=1&prefill_*`, hydrate the initial values so the rep
|
||||
// doesn't retype data they just reviewed. `sourceInquiryId`
|
||||
// gets persisted on save (clients.source_inquiry_id column) so
|
||||
// the inquiry → client lineage survives for the conversion-
|
||||
// funnel chart.
|
||||
const contacts: CreateClientInput['contacts'] = [];
|
||||
if (prefill?.email) {
|
||||
contacts.push({ channel: 'email', value: prefill.email, isPrimary: true });
|
||||
}
|
||||
if (prefill?.phone) {
|
||||
contacts.push({
|
||||
channel: 'phone',
|
||||
value: prefill.phone,
|
||||
isPrimary: contacts.length === 0,
|
||||
});
|
||||
}
|
||||
if (contacts.length === 0) {
|
||||
contacts.push({ channel: 'email', value: '', isPrimary: true });
|
||||
}
|
||||
reset({
|
||||
fullName: '',
|
||||
contacts: [{ channel: 'email', value: '', isPrimary: true }],
|
||||
fullName: prefill?.fullName ?? '',
|
||||
contacts,
|
||||
source: prefill?.source,
|
||||
sourceInquiryId: prefill?.sourceInquiryId,
|
||||
tagIds: [],
|
||||
});
|
||||
}
|
||||
}, [client, open, reset]);
|
||||
}, [client, open, reset, prefill]);
|
||||
|
||||
const mutation = useMutation({
|
||||
mutationFn: async (data: CreateClientInput) => {
|
||||
|
||||
Reference in New Issue
Block a user