From 11ecd5d9008e2a6654849e982e2cd55c22f5753d Mon Sep 17 00:00:00 2001 From: Benjamin Sutter Date: Thu, 21 May 2026 20:47:14 +0200 Subject: [PATCH] fix: need selector on results page + retail budget parsing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Results page: chip strip shows all saved searches — click to switch without creating a new search. Defaults to first need instead of most-recently-created so pre-existing mock needs load immediately on navigation. - AI parser: asset-type-aware monthly budget threshold (RETAIL: 500, others: 150) so "150 CHF/m²" for retail is correctly converted to 1800 CHF/m²/year instead of being stored as an impossibly low annual value that excludes all properties. Co-Authored-By: Claude Sonnet 4.6 --- src/pages/demand/Results.tsx | 35 +++++++++++++++++++++++++++-------- src/services/aiService.ts | 8 +++++--- 2 files changed, 32 insertions(+), 11 deletions(-) diff --git a/src/pages/demand/Results.tsx b/src/pages/demand/Results.tsx index c3a5c55..70eb18e 100644 --- a/src/pages/demand/Results.tsx +++ b/src/pages/demand/Results.tsx @@ -44,6 +44,8 @@ export default function Results() { (localStorage.getItem('view-results') as 'list' | 'grid') ?? 'list' ) + const [selectedNeedId, setSelectedNeedId] = useState(undefined) + // When coming from NeedBuilder, invalidate so the freshly created need is included const activeNeedIdFromNav = (location.state as { activeNeedId?: string } | null)?.activeNeedId @@ -55,14 +57,12 @@ export default function Results() { gcTime: 0, }) - // Prefer the ID passed from NeedBuilder; fall back to most-recently-created - const activeNeed = needResp?.data - ? activeNeedIdFromNav - ? (needResp.data.find(n => n.id === activeNeedIdFromNav) ?? needResp.data[0]) - : [...needResp.data].sort((a, b) => - new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime() - )[0] - : undefined + const allNeeds = needResp?.data ?? [] + + // Priority: explicit user selection → nav (just came from NeedBuilder) → first need in list + const effectiveNeedId = selectedNeedId ?? activeNeedIdFromNav ?? allNeeds[0]?.id + + const activeNeed = allNeeds.find(n => n.id === effectiveNeedId) ?? allNeeds[0] // Ensure query cache is invalidated when a new need was just created if (activeNeedIdFromNav) { @@ -127,6 +127,25 @@ export default function Results() { {activeNeed && ( + {/* Need selector — all saved searches as chips */} + {allNeeds.length > 1 && ( + + {allNeeds.map(n => ( + setSelectedNeedId(n.id)} + sx={n.id === activeNeed.id + ? { fontSize: '0.68rem', height: 22, bgcolor: '#1e3a5f', color: 'white', cursor: 'pointer' } + : { fontSize: '0.68rem', height: 22, color: '#64748b', borderColor: '#cbd5e1', cursor: 'pointer' } + } + /> + ))} + + )} + diff --git a/src/services/aiService.ts b/src/services/aiService.ts index beeaea4..318a416 100644 --- a/src/services/aiService.ts +++ b/src/services/aiService.ts @@ -177,14 +177,16 @@ function mockParseNeed(input: string): ParseNeedResult { const budgetMaxMatch = input.match(/(?:max\.?|bis|höchstens)\s*(?:CHF\s*)?(\d+)/i) let budgetRange: { maxPerSqm: number; currency: string } | undefined let budgetConfidence = 0.20 + // Monthly-to-annual threshold: Swiss retail quotes monthly (150/m²/mon → 1800/year), + // office/logistics quote monthly too but at lower values (45/m²/mon → 540/year). + const monthlyThreshold = assetType === 'RETAIL' ? 500 : 150 if (budgetPerSqmMatch) { const raw = parseInt(budgetPerSqmMatch[1]) - // Values < 100 are monthly (e.g. CHF 45/m²/month); convert to annual - budgetRange = { maxPerSqm: raw < 100 ? raw * 12 : raw, currency: 'CHF' } + budgetRange = { maxPerSqm: raw < monthlyThreshold ? raw * 12 : raw, currency: 'CHF' } budgetConfidence = 0.92 } else if (budgetMaxMatch) { const raw = parseInt(budgetMaxMatch[1]) - budgetRange = { maxPerSqm: raw < 100 ? raw * 12 : raw, currency: 'CHF' } + budgetRange = { maxPerSqm: raw < monthlyThreshold ? raw * 12 : raw, currency: 'CHF' } budgetConfidence = 0.60 }