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:
Benjamin Sutter
2026-05-22 20:31:30 +02:00
parent 98b4a1146b
commit 9f7062d137
16 changed files with 3942 additions and 52 deletions
+8
View File
@@ -68,6 +68,14 @@ function buildNeedInput(
confidenceInCriteria: overallConfidence,
status,
mustCriteriaText: criteria.mustHaveCriteria ?? [],
requireGroundFloor: criteria.requireGroundFloor,
requiredFitOut: criteria.requiredFitOut,
requiredParkingMin: criteria.requiredParkingMin,
requireAirConditioning: criteria.requireAirConditioning,
requireLoadingDock: criteria.requireLoadingDock,
requireBarrierFree: criteria.requireBarrierFree,
minCeilingHeightM: criteria.minCeilingHeightM,
minContractDurationMonths: criteria.minContractDurationMonths,
notes: criteria.notes,
extractedFromText: undefined,
}
+5
View File
@@ -66,6 +66,11 @@ const CRITERION_ALIASES: Record<WeightingKey, string[]> = {
prestige: ['prestige', 'Prestige'],
accessibility: ['accessibility', 'ÖV-Anbindung', 'ÖV', 'Erreichbarkeit'],
expansionPotential:['expansionPotential', 'Expansionspotenzial'],
flexibility: ['flexibility', 'Flexibilität'],
visibility: ['visibility', 'Sichtbarkeit', 'visibilityScore'],
footfall: ['footfall', 'Passantenfrequenz', 'passerbyFrequency'],
talentAccess: ['talentAccess', 'Talent-Zugang', 'Talente'],
esg: ['esg', 'ESG', 'Nachhaltigkeit'],
taxEnvironment: ['taxEnvironment', 'Steuerlast', 'Steuerumfeld'],
}
+9 -7
View File
@@ -39,7 +39,7 @@ export default function Results() {
const [filterSource, setFilterSource] = useState<FilterSource>('ALL')
const [sortBy, setSortBy] = useState<SortBy>('score')
const [showFutureAvailability, setShowFutureAvailability] = useState(true)
const [showOwnProperties, setShowOwnProperties] = useState(false)
const [showOwnProperties, setShowOwnProperties] = useState(true)
const [view, setView] = useState<'list' | 'grid'>(() =>
(localStorage.getItem('view-results') as 'list' | 'grid') ?? 'list'
)
@@ -66,10 +66,12 @@ export default function Results() {
queryClient.invalidateQueries({ queryKey: ['needs'] })
}
const { data: results = [], isLoading } = useUnifiedResults(activeNeed?.id)
const { data: results = [], isLoading } = useUnifiedResults(effectiveNeedId)
const isStaff = true // all roles see portfolio properties in demo
const filtered = results.filter(r => {
if (r.resultType === 'VERIFIED_PORTFOLIO') return showOwnProperties && currentUser?.role === 'PROPERTY_MANAGER'
if (r.resultType === 'VERIFIED_PORTFOLIO') return showOwnProperties
// Future Availability toggle is independent of the source filter
if (r.resultType === 'FUTURE_AVAILABILITY') return showFutureAvailability
return filterSource === 'ALL' || r.resultType === filterSource
@@ -81,7 +83,7 @@ export default function Results() {
const externalCount = results.filter(r => r.resultType === 'EXTERNAL_MARKET').length
const maisonWorkCount = results.filter(r => r.resultType === 'MAISON_WORK').length
const futureCount = results.filter(r => r.resultType === 'FUTURE_AVAILABILITY').length
const strongCount = results.filter(r => r.matchScore >= 80).length
const strongCount = filtered.filter(r => r.matchScore >= 80).length
const missingDataCount = results.filter(r =>
'match' in r && Array.isArray((r as { match?: { missingData?: unknown[] } }).match?.missingData) &&
((r as { match?: { missingData?: unknown[] } }).match?.missingData?.length ?? 0) > 0
@@ -139,12 +141,12 @@ export default function Results() {
<Typography variant="caption" color="text.secondary">
<strong>Standort:</strong> {activeNeed.preferredLocations.join(', ')}
</Typography>
{activeNeed.budgetRange && (
{activeNeed.budgetRange && activeNeed.budgetRange.maxPerSqm > 0 && (
<Typography variant="caption" color="text.secondary">
<strong>Budget:</strong> max. CHF {activeNeed.budgetRange.maxPerSqm}/m²
</Typography>
)}
{activeNeed.timing && (
{activeNeed.timing?.earliestMoveIn && !isNaN(new Date(activeNeed.timing.earliestMoveIn).getTime()) && (
<Typography variant="caption" color="text.secondary">
<strong>Bezug ab:</strong> {new Date(activeNeed.timing.earliestMoveIn).toLocaleDateString('de-CH', { month: 'long', year: 'numeric' })}
</Typography>
@@ -177,7 +179,7 @@ export default function Results() {
onShowFutureAvailabilityChange={setShowFutureAvailability}
showOwnProperties={showOwnProperties}
onShowOwnPropertiesChange={setShowOwnProperties}
isPropertyManager={currentUser?.role === 'PROPERTY_MANAGER'}
isPropertyManager={isStaff}
/>
{isLoading ? (