fix: need selector on results page + retail budget parsing
- 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 <noreply@anthropic.com>
This commit is contained in:
@@ -44,6 +44,8 @@ export default function Results() {
|
|||||||
(localStorage.getItem('view-results') as 'list' | 'grid') ?? 'list'
|
(localStorage.getItem('view-results') as 'list' | 'grid') ?? 'list'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const [selectedNeedId, setSelectedNeedId] = useState<string | undefined>(undefined)
|
||||||
|
|
||||||
// When coming from NeedBuilder, invalidate so the freshly created need is included
|
// When coming from NeedBuilder, invalidate so the freshly created need is included
|
||||||
const activeNeedIdFromNav = (location.state as { activeNeedId?: string } | null)?.activeNeedId
|
const activeNeedIdFromNav = (location.state as { activeNeedId?: string } | null)?.activeNeedId
|
||||||
|
|
||||||
@@ -55,14 +57,12 @@ export default function Results() {
|
|||||||
gcTime: 0,
|
gcTime: 0,
|
||||||
})
|
})
|
||||||
|
|
||||||
// Prefer the ID passed from NeedBuilder; fall back to most-recently-created
|
const allNeeds = needResp?.data ?? []
|
||||||
const activeNeed = needResp?.data
|
|
||||||
? activeNeedIdFromNav
|
// Priority: explicit user selection → nav (just came from NeedBuilder) → first need in list
|
||||||
? (needResp.data.find(n => n.id === activeNeedIdFromNav) ?? needResp.data[0])
|
const effectiveNeedId = selectedNeedId ?? activeNeedIdFromNav ?? allNeeds[0]?.id
|
||||||
: [...needResp.data].sort((a, b) =>
|
|
||||||
new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
|
const activeNeed = allNeeds.find(n => n.id === effectiveNeedId) ?? allNeeds[0]
|
||||||
)[0]
|
|
||||||
: undefined
|
|
||||||
|
|
||||||
// Ensure query cache is invalidated when a new need was just created
|
// Ensure query cache is invalidated when a new need was just created
|
||||||
if (activeNeedIdFromNav) {
|
if (activeNeedIdFromNav) {
|
||||||
@@ -127,6 +127,25 @@ export default function Results() {
|
|||||||
<Box sx={{ flex: 1, overflowY: 'auto', px: 3, py: 3 }}>
|
<Box sx={{ flex: 1, overflowY: 'auto', px: 3, py: 3 }}>
|
||||||
{activeNeed && (
|
{activeNeed && (
|
||||||
<Card sx={{ bgcolor: '#eff6ff', p: 2, mb: 2, border: '1px solid #bfdbfe' }}>
|
<Card sx={{ bgcolor: '#eff6ff', p: 2, mb: 2, border: '1px solid #bfdbfe' }}>
|
||||||
|
{/* Need selector — all saved searches as chips */}
|
||||||
|
{allNeeds.length > 1 && (
|
||||||
|
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 0.75, mb: 1.5 }}>
|
||||||
|
{allNeeds.map(n => (
|
||||||
|
<Chip
|
||||||
|
key={n.id}
|
||||||
|
label={`${n.companyName} · ${n.assetType}`}
|
||||||
|
size="small"
|
||||||
|
variant={n.id === activeNeed.id ? 'filled' : 'outlined'}
|
||||||
|
onClick={() => 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' }
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
|
||||||
<Box sx={{ display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between', gap: 2 }}>
|
<Box sx={{ display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between', gap: 2 }}>
|
||||||
<Box>
|
<Box>
|
||||||
<Typography variant="subtitle2" sx={{ fontWeight: 700, mb: 0.5 }} color="#1e3a5f">
|
<Typography variant="subtitle2" sx={{ fontWeight: 700, mb: 0.5 }} color="#1e3a5f">
|
||||||
|
|||||||
@@ -177,14 +177,16 @@ function mockParseNeed(input: string): ParseNeedResult {
|
|||||||
const budgetMaxMatch = input.match(/(?:max\.?|bis|höchstens)\s*(?:CHF\s*)?(\d+)/i)
|
const budgetMaxMatch = input.match(/(?:max\.?|bis|höchstens)\s*(?:CHF\s*)?(\d+)/i)
|
||||||
let budgetRange: { maxPerSqm: number; currency: string } | undefined
|
let budgetRange: { maxPerSqm: number; currency: string } | undefined
|
||||||
let budgetConfidence = 0.20
|
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) {
|
if (budgetPerSqmMatch) {
|
||||||
const raw = parseInt(budgetPerSqmMatch[1])
|
const raw = parseInt(budgetPerSqmMatch[1])
|
||||||
// Values < 100 are monthly (e.g. CHF 45/m²/month); convert to annual
|
budgetRange = { maxPerSqm: raw < monthlyThreshold ? raw * 12 : raw, currency: 'CHF' }
|
||||||
budgetRange = { maxPerSqm: raw < 100 ? raw * 12 : raw, currency: 'CHF' }
|
|
||||||
budgetConfidence = 0.92
|
budgetConfidence = 0.92
|
||||||
} else if (budgetMaxMatch) {
|
} else if (budgetMaxMatch) {
|
||||||
const raw = parseInt(budgetMaxMatch[1])
|
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
|
budgetConfidence = 0.60
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user