feat: UC-A/B/C demo — 50 mock properties, district scoring, gold/silver cards visible
- Add 50 mock properties (prop-050–099) tuned for UC-A (Fahrradhändler RETAIL),
UC-B (Umzugsfirma OFFICE Zürich-West) and UC-C (Vermögensverwalter PREMIUM)
- District-aware scoreLocation: district match→100, city-only→70 when need
specifies districts, canton→60, no match→35; fixes UC-C prop-001 over-scoring
- mustHaveScorer: add klimatisierung keyword; parking minimum count logic
- Soft factor scale fix: integer 0–100 values no longer multiplied ×100
- footfall: map passerbyFrequency string (HIGH→85, MEDIUM_HIGH→68…) before
enrichment fallback so RETAIL properties score correctly
- Parser: Kreis list extraction ("Kreis 3, 4, 5, und 8" → 4 district entries),
neighbourhood→district map (Seefeld, Bahnhofstrasse), prestige signals
- Results: remove VERIFIED_PORTFOLIO role gate — all users see portfolio cards,
enabling gold (85+) and silver (70–84) cards for every demo use case
- Fix flash of wrong cards on NeedBuilder nav (effectiveNeedId not activeNeed?.id)
- needs.ts: UC-A preferredLocations now includes Kreis 3/4/5/8 entries
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import { useState } from 'react'
|
||||
import { Box, Card, Chip, Stack, TextField, Typography } from '@mui/material'
|
||||
import { Accordion, AccordionDetails, AccordionSummary, Box, Card, Checkbox, Chip, FormControlLabel, MenuItem, Stack, TextField, Typography } from '@mui/material'
|
||||
import { ChevronDown } from 'lucide-react'
|
||||
import type { ParsedNeedCriteria } from '../../domain/needBuilder'
|
||||
import { AssetType } from '../../domain/enums'
|
||||
|
||||
@@ -210,7 +211,7 @@ export function NeedInput({ criteria: c, onCriteriaChange: set }: Props) {
|
||||
sx={{ mb: 0.75 }}
|
||||
/>
|
||||
{(c.mustHaveCriteria?.length ?? 0) > 0 && (
|
||||
<Stack direction="row" spacing={0.5} sx={{ flexWrap: 'wrap', gap: 0.5 }}>
|
||||
<Stack direction="row" spacing={0.5} sx={{ flexWrap: 'wrap', gap: 0.5, mb: 1.5 }}>
|
||||
{c.mustHaveCriteria!.map(item => (
|
||||
<Chip key={item} label={item} size="small"
|
||||
onDelete={() => set({ ...c, mustHaveCriteria: c.mustHaveCriteria!.filter(m => m !== item) })}
|
||||
@@ -218,6 +219,78 @@ export function NeedInput({ criteria: c, onCriteriaChange: set }: Props) {
|
||||
))}
|
||||
</Stack>
|
||||
)}
|
||||
|
||||
{/* Extended requirements */}
|
||||
<Accordion disableGutters elevation={0} sx={{ border: '1px solid #e2e8f0', borderRadius: 1, mt: 1, '&:before': { display: 'none' } }}>
|
||||
<AccordionSummary expandIcon={<ChevronDown size={16} />} sx={{ minHeight: 36, px: 1.5, '& .MuiAccordionSummary-content': { my: 0.5 } }}>
|
||||
<Typography variant="caption" sx={{ fontWeight: 600, color: '#64748b' }}>Erweiterte Anforderungen</Typography>
|
||||
</AccordionSummary>
|
||||
<AccordionDetails sx={{ px: 1.5, pt: 0, pb: 1.5 }}>
|
||||
{/* Boolean requirement checkboxes — 2×2 grid */}
|
||||
<Box sx={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 0.5, mb: 1.5 }}>
|
||||
<FormControlLabel
|
||||
control={<Checkbox size="small" checked={c.requireGroundFloor ?? false} onChange={e => set({ ...c, requireGroundFloor: e.target.checked || undefined })} />}
|
||||
label={<Typography variant="caption">Erdgeschoss erforderlich</Typography>}
|
||||
/>
|
||||
<FormControlLabel
|
||||
control={<Checkbox size="small" checked={c.requireAirConditioning ?? false} onChange={e => set({ ...c, requireAirConditioning: e.target.checked || undefined })} />}
|
||||
label={<Typography variant="caption">Klimaanlage erforderlich</Typography>}
|
||||
/>
|
||||
<FormControlLabel
|
||||
control={<Checkbox size="small" checked={c.requireLoadingDock ?? false} onChange={e => set({ ...c, requireLoadingDock: e.target.checked || undefined })} />}
|
||||
label={<Typography variant="caption">Laderampe erforderlich</Typography>}
|
||||
/>
|
||||
<FormControlLabel
|
||||
control={<Checkbox size="small" checked={c.requireBarrierFree ?? false} onChange={e => set({ ...c, requireBarrierFree: e.target.checked || undefined })} />}
|
||||
label={<Typography variant="caption">Barrierefrei erforderlich</Typography>}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
{/* Numeric / select fields — 2×2 grid */}
|
||||
<Box sx={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 1.5 }}>
|
||||
<Box>
|
||||
<Typography variant="caption" color="text.secondary" sx={{ display: 'block', mb: 0.5 }}>Mindest-Parkplätze</Typography>
|
||||
<TextField
|
||||
size="small" type="number" fullWidth placeholder="0"
|
||||
value={c.requiredParkingMin ?? ''}
|
||||
onChange={e => set({ ...c, requiredParkingMin: parseInt(e.target.value) || undefined })}
|
||||
slotProps={{ htmlInput: { min: 0, max: 100 } }}
|
||||
/>
|
||||
</Box>
|
||||
<Box>
|
||||
<Typography variant="caption" color="text.secondary" sx={{ display: 'block', mb: 0.5 }}>Mindest-Ausbaustandard</Typography>
|
||||
<TextField
|
||||
select size="small" fullWidth
|
||||
value={c.requiredFitOut ?? ''}
|
||||
onChange={e => set({ ...c, requiredFitOut: (e.target.value as 'BASIC' | 'FULL' | 'PREMIUM') || undefined })}
|
||||
>
|
||||
<MenuItem value="">Kein Mindeststandard</MenuItem>
|
||||
<MenuItem value="BASIC">Basisausbau</MenuItem>
|
||||
<MenuItem value="FULL">Vollausbau</MenuItem>
|
||||
<MenuItem value="PREMIUM">Premiumausbau</MenuItem>
|
||||
</TextField>
|
||||
</Box>
|
||||
<Box>
|
||||
<Typography variant="caption" color="text.secondary" sx={{ display: 'block', mb: 0.5 }}>Mindest-Deckenhöhe (m)</Typography>
|
||||
<TextField
|
||||
size="small" type="number" fullWidth placeholder="z.B. 6"
|
||||
value={c.minCeilingHeightM ?? ''}
|
||||
onChange={e => set({ ...c, minCeilingHeightM: parseFloat(e.target.value) || undefined })}
|
||||
slotProps={{ htmlInput: { min: 2, max: 20, step: 0.5 } }}
|
||||
/>
|
||||
</Box>
|
||||
<Box>
|
||||
<Typography variant="caption" color="text.secondary" sx={{ display: 'block', mb: 0.5 }}>Mindest-Vertragslaufzeit (Monate)</Typography>
|
||||
<TextField
|
||||
size="small" type="number" fullWidth placeholder="z.B. 36"
|
||||
value={c.minContractDurationMonths ?? ''}
|
||||
onChange={e => set({ ...c, minContractDurationMonths: parseInt(e.target.value) || undefined })}
|
||||
slotProps={{ htmlInput: { min: 0, max: 360, step: 12 } }}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
</AccordionDetails>
|
||||
</Accordion>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Box, Chip, Paper, Typography } from '@mui/material'
|
||||
import { CheckCircle2, XCircle, Minus } from 'lucide-react'
|
||||
import type { Match } from '../../domain/match'
|
||||
import { Box, Paper, Tooltip, Typography } from '@mui/material'
|
||||
import { CheckCircle2, XCircle, HelpCircle, Minus } from 'lucide-react'
|
||||
import type { Match, MustHaveResult } from '../../domain/match'
|
||||
import type { Property } from '../../domain/property'
|
||||
import type { Need } from '../../domain/need'
|
||||
|
||||
@@ -90,11 +90,30 @@ interface Props {
|
||||
property: Property | null
|
||||
}
|
||||
|
||||
export function NeedAlignmentPanel({ match: _match, need, property }: Props) {
|
||||
function mustHaveIcon(r: MustHaveResult) {
|
||||
if (r.confidence === 'UNKNOWN') return <HelpCircle size={15} color="#d97706" />
|
||||
if (r.passed) return <CheckCircle2 size={15} color="#1a7a4a" />
|
||||
return <XCircle size={15} color="#c0392b" />
|
||||
}
|
||||
|
||||
function mustHaveBg(r: MustHaveResult): string {
|
||||
if (r.confidence === 'UNKNOWN') return '#fffbeb'
|
||||
if (r.passed) return '#f0fdf4'
|
||||
return '#fef2f2'
|
||||
}
|
||||
|
||||
export function NeedAlignmentPanel({ match, need, property }: Props) {
|
||||
if (!need || !property) return null
|
||||
|
||||
const rows = buildRows(need, property)
|
||||
const mustHaves = need.mustHaveCriteria ?? []
|
||||
const evaluated = match.mustHaveEvaluation ?? []
|
||||
const rawTexts = [
|
||||
...(need.mustCriteriaText ?? []),
|
||||
...(need.mustHaveCriteria?.map(c => c.criterion) ?? []),
|
||||
]
|
||||
// Fall back to unevaluated chips if engine didn't produce evaluation yet
|
||||
const showEvaluated = evaluated.length > 0
|
||||
const showFallback = !showEvaluated && rawTexts.length > 0
|
||||
|
||||
return (
|
||||
<Paper sx={{ p: 2.5, mb: 2 }}>
|
||||
@@ -135,15 +154,44 @@ export function NeedAlignmentPanel({ match: _match, need, property }: Props) {
|
||||
))}
|
||||
</Box>
|
||||
|
||||
{/* Must-haves */}
|
||||
{mustHaves.length > 0 && (
|
||||
{/* Evaluated must-haves */}
|
||||
{showEvaluated && (
|
||||
<Box>
|
||||
<Typography variant="caption" sx={{ fontWeight: 600, color: '#64748b', display: 'block', mb: 0.75 }}>
|
||||
Must-have Kriterien
|
||||
</Typography>
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.5 }}>
|
||||
{evaluated.map((r, i) => (
|
||||
<Tooltip key={i} title={r.explanation} arrow placement="right">
|
||||
<Box sx={{
|
||||
display: 'flex', alignItems: 'center', gap: 1,
|
||||
px: 1.25, py: 0.6, borderRadius: 0.75,
|
||||
bgcolor: mustHaveBg(r), cursor: 'default',
|
||||
}}>
|
||||
{mustHaveIcon(r)}
|
||||
<Typography variant="body2" sx={{ flex: 1, fontSize: '0.82rem' }}>{r.criterion}</Typography>
|
||||
<Typography variant="caption" sx={{ color: '#94a3b8', fontSize: '0.70rem' }}>
|
||||
{r.confidence === 'UNKNOWN' ? 'Nicht prüfbar' : r.passed ? 'Erfüllt' : 'Nicht erfüllt'}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Tooltip>
|
||||
))}
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* Fallback: unevaluated must-haves (no criteria text to evaluate) */}
|
||||
{showFallback && (
|
||||
<Box>
|
||||
<Typography variant="caption" sx={{ fontWeight: 600, color: '#64748b', display: 'block', mb: 0.75 }}>
|
||||
Must-have Kriterien
|
||||
</Typography>
|
||||
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 0.5 }}>
|
||||
{mustHaves.map((m, i) => (
|
||||
<Chip key={i} label={m.criterion} size="small" variant="outlined" />
|
||||
{rawTexts.map((t, i) => (
|
||||
<Box key={i} sx={{ display: 'flex', alignItems: 'center', gap: 0.5, px: 1, py: 0.4, bgcolor: '#f1f5f9', borderRadius: 0.75, border: '1px solid #e2e8f0' }}>
|
||||
<Minus size={12} color="#94a3b8" />
|
||||
<Typography variant="caption">{t}</Typography>
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
Reference in New Issue
Block a user