feat(search): pipeline-stage fuzzy match shortcut
Typing a stage name in the topbar search now surfaces a "Stage: <Label>"
shortcut row that lands the rep on the interests list filtered by that
stage. Previously reps had to know the navigation path and either click
through the kanban board or hand-type the URL filter.
Match flavours (case-insensitive, query tokens split on whitespace):
1. Modern label prefix — every query token must prefix a token in
`STAGE_LABELS[stage]` or the raw enum slug. "eoi" → EOI, "dep" →
Deposit Paid, "qua" → Qualified.
2. Stage-key substring on the raw enum slug.
3. Legacy aliases via `LEGACY_STAGE_REMAP` — "eoi_signed" /
"deposit_10pct" / "contract_signed" lands on the modern 7-stage
equivalent so reps with muscle memory still find a useful target.
Each row carries a live COUNT(*) of non-archived interests in that
stage (single grouped query — O(stages)). Empty queries skip the
bucket entirely.
- `searchStages(portId, query, limit)` in search.service.ts with the
scoring logic + count query.
- New `StageSuggestionResult` type added to SearchResults + the
client-side mirror in use-search.ts.
- `searchStages` wired into the parallel `Promise.all` block of the
main `search()` and the single-bucket runSingleBucket dispatch
(exhaustive ts-pattern match required the new branch).
- Gated on `interests.view` — destination of the filter.
- New 'stages' bucket in command-search.tsx BUCKETS list (between
Tags and Notes) + a `buildFlatRows` arm that pushes one row per
matched stage. Mobile overlay reuses `buildFlatRows`, so the new
rows appear there too once BUCKET_LABELS picks up the entry.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -72,6 +72,11 @@ const BUCKETS: BucketConfig[] = [
|
||||
{ type: 'reminders', label: 'Reminders', icon: Bell },
|
||||
{ type: 'brochures', label: 'Brochures', icon: Camera },
|
||||
{ type: 'tags', label: 'Tags', icon: TagIcon },
|
||||
// Pipeline-stage shortcuts ("eoi sent" → /interests?pipelineStage=eoi).
|
||||
// Sits below Tags so direct entity matches always win the dropdown;
|
||||
// stage filters are macro affordances that surface when the rep types
|
||||
// a stage name verbatim.
|
||||
{ type: 'stages', label: 'Stages', icon: TrendingUp },
|
||||
// Notes are noisy content search.
|
||||
{ type: 'notes', label: 'Notes', icon: MessageSquare },
|
||||
// Navigation (settings pages + admin sub-cards) lives at the very bottom —
|
||||
@@ -1123,6 +1128,23 @@ export function buildFlatRows(args: BuildFlatRowsArgs): FlatRow[] {
|
||||
});
|
||||
}
|
||||
}
|
||||
// Stage-filter shortcuts. Typing a stage label ("eoi sent", "deposit",
|
||||
// "qualified") surfaces a row that lands the rep on the filtered
|
||||
// interests list. count is the live row total so the rep sees how
|
||||
// many records they're about to scope to.
|
||||
if (include('stages')) {
|
||||
for (const st of results.stages) {
|
||||
rows.push({
|
||||
kind: 'result',
|
||||
key: `stages:${st.stage}`,
|
||||
bucket: 'stages',
|
||||
icon: TrendingUp,
|
||||
label: `Stage: ${st.label}`,
|
||||
sub: `${st.count} ${st.count === 1 ? 'interest' : 'interests'} in this stage`,
|
||||
href: `/${portSlug}${st.href}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
// Notes — content matches inside free-text notes are noisy by nature, so
|
||||
// the user sees them after the entity-specific buckets above have
|
||||
// surfaced their tighter matches.
|
||||
|
||||
@@ -548,6 +548,7 @@ const BUCKET_LABELS: Record<BucketType, string> = {
|
||||
tags: 'Tags',
|
||||
navigation: 'Settings & navigation',
|
||||
notes: 'Notes',
|
||||
stages: 'Stages',
|
||||
};
|
||||
|
||||
function Section({
|
||||
|
||||
Reference in New Issue
Block a user