From 9f7062d1375df90f133b0f511da744f6f7afa7fb Mon Sep 17 00:00:00 2001 From: Benjamin Sutter Date: Fri, 22 May 2026 20:31:30 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20UC-A/B/C=20demo=20=E2=80=94=2050=20mock?= =?UTF-8?q?=20properties,=20district=20scoring,=20gold/silver=20cards=20vi?= =?UTF-8?q?sible?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- src/components/demand/NeedInput.tsx | 77 +- .../match-detail/NeedAlignmentPanel.tsx | 66 +- src/domain/match.ts | 12 + src/domain/need.ts | 11 + src/domain/needBuilder.ts | 8 + src/domain/property.ts | 2 + src/domain/scoring.ts | 3 +- src/features/matching/mustHaveScorer.ts | 205 ++ src/features/matching/scoreCalculator.ts | 219 +- src/mock-data/needs.ts | 104 + src/mock-data/properties.ts | 3047 +++++++++++++++++ src/pages/demand/AISearch.tsx | 8 + src/pages/demand/Compare.tsx | 5 + src/pages/demand/Results.tsx | 16 +- src/provider/MockupNeedProvider.ts | 1 + src/services/aiService.ts | 210 +- 16 files changed, 3942 insertions(+), 52 deletions(-) create mode 100644 src/features/matching/mustHaveScorer.ts diff --git a/src/components/demand/NeedInput.tsx b/src/components/demand/NeedInput.tsx index bd0601c..655d58c 100644 --- a/src/components/demand/NeedInput.tsx +++ b/src/components/demand/NeedInput.tsx @@ -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 && ( - + {c.mustHaveCriteria!.map(item => ( set({ ...c, mustHaveCriteria: c.mustHaveCriteria!.filter(m => m !== item) })} @@ -218,6 +219,78 @@ export function NeedInput({ criteria: c, onCriteriaChange: set }: Props) { ))} )} + + {/* Extended requirements */} + + } sx={{ minHeight: 36, px: 1.5, '& .MuiAccordionSummary-content': { my: 0.5 } }}> + Erweiterte Anforderungen + + + {/* Boolean requirement checkboxes — 2×2 grid */} + + set({ ...c, requireGroundFloor: e.target.checked || undefined })} />} + label={Erdgeschoss erforderlich} + /> + set({ ...c, requireAirConditioning: e.target.checked || undefined })} />} + label={Klimaanlage erforderlich} + /> + set({ ...c, requireLoadingDock: e.target.checked || undefined })} />} + label={Laderampe erforderlich} + /> + set({ ...c, requireBarrierFree: e.target.checked || undefined })} />} + label={Barrierefrei erforderlich} + /> + + + {/* Numeric / select fields — 2×2 grid */} + + + Mindest-Parkplätze + set({ ...c, requiredParkingMin: parseInt(e.target.value) || undefined })} + slotProps={{ htmlInput: { min: 0, max: 100 } }} + /> + + + Mindest-Ausbaustandard + set({ ...c, requiredFitOut: (e.target.value as 'BASIC' | 'FULL' | 'PREMIUM') || undefined })} + > + Kein Mindeststandard + Basisausbau + Vollausbau + Premiumausbau + + + + Mindest-Deckenhöhe (m) + set({ ...c, minCeilingHeightM: parseFloat(e.target.value) || undefined })} + slotProps={{ htmlInput: { min: 2, max: 20, step: 0.5 } }} + /> + + + Mindest-Vertragslaufzeit (Monate) + set({ ...c, minContractDurationMonths: parseInt(e.target.value) || undefined })} + slotProps={{ htmlInput: { min: 0, max: 360, step: 12 } }} + /> + + + + ) } diff --git a/src/components/match-detail/NeedAlignmentPanel.tsx b/src/components/match-detail/NeedAlignmentPanel.tsx index 0ba6fbc..52d30f7 100644 --- a/src/components/match-detail/NeedAlignmentPanel.tsx +++ b/src/components/match-detail/NeedAlignmentPanel.tsx @@ -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 + if (r.passed) return + return +} + +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 ( @@ -135,15 +154,44 @@ export function NeedAlignmentPanel({ match: _match, need, property }: Props) { ))} - {/* Must-haves */} - {mustHaves.length > 0 && ( + {/* Evaluated must-haves */} + {showEvaluated && ( + + + Must-have Kriterien + + + {evaluated.map((r, i) => ( + + + {mustHaveIcon(r)} + {r.criterion} + + {r.confidence === 'UNKNOWN' ? 'Nicht prüfbar' : r.passed ? 'Erfüllt' : 'Nicht erfüllt'} + + + + ))} + + + )} + + {/* Fallback: unevaluated must-haves (no criteria text to evaluate) */} + {showFallback && ( Must-have Kriterien - {mustHaves.map((m, i) => ( - + {rawTexts.map((t, i) => ( + + + {t} + ))} diff --git a/src/domain/match.ts b/src/domain/match.ts index 8cc88c2..8ee3fd9 100644 --- a/src/domain/match.ts +++ b/src/domain/match.ts @@ -17,6 +17,15 @@ export interface PropertyNeedMatch { matchHighlights: string[] } +// ── Must-Have Evaluation ───────────────────────────────────────────────────── + +export interface MustHaveResult { + criterion: string + passed: boolean + confidence: 'CERTAIN' | 'ESTIMATED' | 'UNKNOWN' + explanation: string +} + // ── Score Building Blocks ───────────────────────────────────────────────────── export interface ScoreBreakdown { @@ -115,6 +124,9 @@ export interface Match { riskLevel: RiskLevel uncertaintyIndicators: string[] + // Must-have evaluation results (from mustHaveScorer) + mustHaveEvaluation?: MustHaveResult[] + // Alternatives alternativeStrategies?: AlternativeStrategy[] diff --git a/src/domain/need.ts b/src/domain/need.ts index 66e5e5e..ade1a2f 100644 --- a/src/domain/need.ts +++ b/src/domain/need.ts @@ -82,6 +82,17 @@ export interface Need { budgetRange: BudgetRange timing: Timing mustCriteriaText?: string[] // legacy — prefer mustHaveCriteria + + // Structured requirement flags + requireGroundFloor?: boolean + requiredFitOut?: 'BASIC' | 'FULL' | 'PREMIUM' + requiredParkingMin?: number + requireAirConditioning?: boolean + requireLoadingDock?: boolean + requireBarrierFree?: boolean + minCeilingHeightM?: number + minContractDurationMonths?: number + softFactors?: SoftFactorPreferences weightingProfile: WeightingProfile confidenceInCriteria: number diff --git a/src/domain/needBuilder.ts b/src/domain/needBuilder.ts index 2cf99c4..e277c72 100644 --- a/src/domain/needBuilder.ts +++ b/src/domain/needBuilder.ts @@ -8,6 +8,14 @@ export interface ParsedNeedCriteria { timing?: { earliestMoveIn: string; latestMoveIn?: string; contractDurationMonths?: number; flexibleTiming: boolean } mustHaveCriteria?: string[] softFactors?: { minPrestige?: number; requireParking?: boolean; maxPublicTransportMinutes?: number; requireHighVisibility?: boolean } + requireGroundFloor?: boolean + requiredFitOut?: 'BASIC' | 'FULL' | 'PREMIUM' + requiredParkingMin?: number + requireAirConditioning?: boolean + requireLoadingDock?: boolean + requireBarrierFree?: boolean + minCeilingHeightM?: number + minContractDurationMonths?: number infrastructureRequirements?: string[] accessibilityRequirements?: string[] prestigeImportance?: 'LOW' | 'MEDIUM' | 'HIGH' diff --git a/src/domain/property.ts b/src/domain/property.ts index 82e62bb..944d8f9 100644 --- a/src/domain/property.ts +++ b/src/domain/property.ts @@ -61,6 +61,8 @@ export interface PropertyHardFacts { powerSupplyKva?: number hasServerRoom?: boolean isBarrierFree?: boolean + hasAirConditioning?: boolean + hasStorefront?: boolean } // ── Soft Factors ────────────────────────────────────────────────────────────── diff --git a/src/domain/scoring.ts b/src/domain/scoring.ts index 7fbbc6d..eda6360 100644 --- a/src/domain/scoring.ts +++ b/src/domain/scoring.ts @@ -1,4 +1,4 @@ -import type { ScoreFactor, TradeOff, Risk, MissingDataItem, NextBestAction } from './match' +import type { ScoreFactor, TradeOff, Risk, MissingDataItem, NextBestAction, MustHaveResult } from './match' // ── Hard Filter Thresholds ──────────────────────────────────────────────────── @@ -141,4 +141,5 @@ export interface MatchEngineOutput { risks: Risk[] missingData: MissingDataItem[] nextBestActions: NextBestAction[] + mustHaveEvaluation?: MustHaveResult[] } diff --git a/src/features/matching/mustHaveScorer.ts b/src/features/matching/mustHaveScorer.ts new file mode 100644 index 0000000..1cd2bee --- /dev/null +++ b/src/features/matching/mustHaveScorer.ts @@ -0,0 +1,205 @@ +import type { Property } from '../../domain/property' +import type { MustHaveResult } from '../../domain/match' + +// ── Penalty per failed CERTAIN criterion (applied to totalScore) ────────────── +export const MUST_HAVE_PENALTY_PER_MISS = 8 +export const MUST_HAVE_MAX_PENALTY = 24 + +interface KeywordRule { + keywords: string[] + check: (p: Property) => boolean | null // null = data not available + explanation: (p: Property, passed: boolean) => string +} + +function parking(p: Property): number { + return p.hardFacts?.parking ?? p.softFactors?.parkingSpots ?? 0 +} + +const RULES: KeywordRule[] = [ + { + keywords: ['schaufenster', 'shopfenster', 'shopfront', 'ladenlokal', 'vitrine'], + check: (p) => p.hardFacts?.hasStorefront ?? null, + explanation: (p, ok) => + p.hardFacts?.hasStorefront === undefined + ? 'Keine Daten zum Schaufenster verfügbar' + : ok ? 'Schaufenster / Shopfront bestätigt' : 'Kein Schaufenster vorhanden', + }, + { + keywords: ['erdgeschoss', 'parterre', 'ground floor', 'eg ', 'eg,', '(eg)'], + check: (p) => { + const floor = p.hardFacts?.floor ?? p.floorLevel + if (floor === undefined) return null + return floor === 0 + }, + explanation: (p, ok) => { + const floor = p.hardFacts?.floor ?? p.floorLevel + if (floor === undefined) return 'Stockwerk nicht bekannt' + return ok ? `Erdgeschoss bestätigt (Etage ${floor})` : `Nicht Erdgeschoss (Etage ${floor})` + }, + }, + { + keywords: ['klimaanlage', 'air conditioning', 'aircondition', 'klima ', 'kühlung', 'hvac', 'klimatisierung', 'klimatisier'], + check: (p) => p.hardFacts?.hasAirConditioning ?? null, + explanation: (p, ok) => + p.hardFacts?.hasAirConditioning === undefined + ? 'Klimaanlage nicht dokumentiert' + : ok ? 'Klimaanlage vorhanden' : 'Keine Klimaanlage vorhanden', + }, + { + keywords: ['laderampe', 'loading dock', 'rampe', 'verladerampe', 'tor'], + check: (p) => { + const docks = p.hardFacts?.loadingDocksCount + if (docks === undefined) return null + return docks > 0 + }, + explanation: (p, ok) => { + const docks = p.hardFacts?.loadingDocksCount + if (docks === undefined) return 'Laderampe / Tor nicht dokumentiert' + return ok ? `${docks} Laderampe(n) vorhanden` : 'Keine Laderampe vorhanden' + }, + }, + { + keywords: ['parkplatz', 'parkplätze', 'parking', 'stellplatz', 'stellplätze', 'autoabstellplatz'], + check: (p) => { + const count = parking(p) + return count > 0 + }, + explanation: (p, ok) => { + const count = parking(p) + if (count === 0) return 'Kein Parkplatz vorhanden oder keine Daten' + return ok ? `${count} Parkplatz/Parkplätze vorhanden` : 'Kein Parkplatz' + }, + }, + { + keywords: ['barrierefrei', 'rollstuhl', 'handicap', 'behindertengerecht', 'iv-gerecht'], + check: (p) => p.hardFacts?.isBarrierFree ?? null, + explanation: (p, ok) => + p.hardFacts?.isBarrierFree === undefined + ? 'Barrierefreiheit nicht dokumentiert' + : ok ? 'Barrierefrei bestätigt' : 'Nicht barrierefrei', + }, + { + keywords: ['serverraum', 'rechenzentrum', 'server room', 'datacenter', 'it-raum'], + check: (p) => p.hardFacts?.hasServerRoom ?? null, + explanation: (p, ok) => + p.hardFacts?.hasServerRoom === undefined + ? 'Serverraum nicht dokumentiert' + : ok ? 'Serverraum vorhanden' : 'Kein Serverraum', + }, + { + keywords: ['langfristig', 'festvertrag', 'langzeitmiete', '10 jahre', '10-jahres', 'langfristiger vertrag'], + check: (p) => { + const dur = p.contractDurationMonths + if (dur === undefined) return null + return dur >= 120 + }, + explanation: (p, ok) => { + const dur = p.contractDurationMonths + if (dur === undefined) return 'Vertragsdauer nicht bekannt' + return ok + ? `Langfristige Vermietung möglich (${Math.round(dur / 12)} Jahre)` + : `Nur bis ${Math.round(dur / 12)} Jahre — langfristiger Vertrag evtl. nicht möglich` + }, + }, + { + keywords: ['tageslicht', 'fensterfront', 'natural light', 'natürliches licht', 'aussenfenster'], + check: (_p) => null, // no structured field yet — always UNKNOWN + explanation: (_p, _ok) => 'Tageslicht / Fensterfront nicht strukturiert erfasst', + }, +] + +// ── Number extraction helpers ────────────────────────────────────────────────── + +function extractNumber(text: string): number | null { + const m = text.match(/(\d+)/) + return m ? parseInt(m[1], 10) : null +} + +function isParkingKeyword(text: string): boolean { + return RULES[4].keywords.some(kw => text.toLowerCase().includes(kw)) +} + +// ── Main scorer ─────────────────────────────────────────────────────────────── + +export function scoreMustHaves( + criteria: string[], + property: Property, +): { + results: MustHaveResult[] + passedCount: number + totalCount: number + scoreImpact: number +} { + if (criteria.length === 0) { + return { results: [], passedCount: 0, totalCount: 0, scoreImpact: 0 } + } + + const results: MustHaveResult[] = criteria.map((raw) => { + const lower = raw.toLowerCase() + + // Special case: parking with minimum count ("mind. 5 parkplätze") + if (isParkingKeyword(lower)) { + const required = extractNumber(lower) + const available = parking(property) + if (required !== null) { + const passed = available >= required + return { + criterion: raw, + passed, + confidence: 'CERTAIN' as const, + explanation: passed + ? `${available} Parkplätze vorhanden (mind. ${required} verlangt)` + : `Nur ${available} Parkplätze (mind. ${required} verlangt)`, + } + } + // No number: just check if any parking available + const hasParking = available > 0 + return { + criterion: raw, + passed: hasParking, + confidence: available === 0 ? 'UNKNOWN' as const : 'CERTAIN' as const, + explanation: hasParking ? `${available} Parkplatz/Parkplätze vorhanden` : 'Keine Parkplätze dokumentiert', + } + } + + for (const rule of RULES) { + if (rule.keywords.some(kw => lower.includes(kw))) { + const result = rule.check(property) + if (result === null) { + return { + criterion: raw, + passed: false, + confidence: 'UNKNOWN' as const, + explanation: rule.explanation(property, false), + } + } + return { + criterion: raw, + passed: result, + confidence: 'CERTAIN' as const, + explanation: rule.explanation(property, result), + } + } + } + + // No keyword matched + return { + criterion: raw, + passed: false, + confidence: 'UNKNOWN' as const, + explanation: 'Kriterium konnte nicht automatisch geprüft werden', + } + }) + + const certain = results.filter(r => r.confidence === 'CERTAIN') + const passedCount = results.filter(r => r.passed && r.confidence !== 'UNKNOWN').length + const failedCertain = certain.filter(r => !r.passed).length + const scoreImpact = -Math.min(MUST_HAVE_MAX_PENALTY, failedCertain * MUST_HAVE_PENALTY_PER_MISS) + + return { + results, + passedCount, + totalCount: certain.length, + scoreImpact, + } +} diff --git a/src/features/matching/scoreCalculator.ts b/src/features/matching/scoreCalculator.ts index a70880e..6703323 100644 --- a/src/features/matching/scoreCalculator.ts +++ b/src/features/matching/scoreCalculator.ts @@ -14,6 +14,7 @@ import type { ScoringWeightProfile, HardFilterResult, MatchEngineOutput, SoftFac import { analyzeTradeOffs, analyzeRisks, identifyMissingData } from './tradeOffAnalyzer' import { generateNextBestActions } from './rankingEngine' import { softFactorEnrichmentService } from '../../services/softFactorEnrichmentService' +import { scoreMustHaves } from './mustHaveScorer' // ── Profile resolution ──────────────────────────────────────────────────────── @@ -117,6 +118,7 @@ function scoreArea(need: Need, property: Property, weight: number): ScoreFactor function scoreLocation(need: Need, property: Property, weight: number): ScoreFactor { const city = property.location.city.toLowerCase() + const district = (property.location.district ?? '').toLowerCase() const canton = (property.location.canton ?? '').toLowerCase() const preferred = (need.preferredLocations ?? []).map(l => l.toLowerCase()) @@ -124,11 +126,26 @@ function scoreLocation(need: Need, property: Property, weight: number): ScoreFac let explanation: string if (preferred.length === 0) { - score = 70 - explanation = 'Kein Standortwunsch — neutral bewertet' - } else if (preferred.some(p => city.includes(p) || p.includes(city))) { + score = 80 + explanation = 'Kein Standortwunsch — flexibel bewertet' + } else if (district && preferred.some(p => { + // "Zürich Kreis 5".includes("Kreis 5") or pSuffix "kreis 5" ⊂ district + if (p.includes(district)) return true + const pSuffix = p.replace(/^zürich\s+/i, '').trim() + // Only use pSuffix when the regex actually removed a prefix (avoids 'zürich' falsely + // matching district names like 'Zürich-West' because 'Zürich-West'.includes('zürich')) + return pSuffix !== p && pSuffix.length > 0 && district.includes(pSuffix) + })) { score = 100 - explanation = `Standort ${property.location.city} entspricht Präferenz` + explanation = `Bezirk ${property.location.district} entspricht Präferenz` + } else if (preferred.some(p => city.includes(p) || p.includes(city))) { + // City matches but check if preferred has district-specific entries for this city + // → penalise when a specific district was requested but this property is in a different one + const hasDistrictSpecifics = preferred.some(p => p.includes(city) && p.length > city.length + 2) + score = hasDistrictSpecifics ? 70 : 100 + explanation = score === 100 + ? `Standort ${property.location.city} entspricht Präferenz` + : `${property.location.city}${property.location.district ? ` (${property.location.district})` : ''} — Lage akzeptiert, bevorzugter Bezirk nicht erfüllt` } else if (canton && preferred.some(p => p.includes(canton) || canton.includes(p))) { score = 60 explanation = `Gleicher Kanton wie Präferenz (${property.location.canton})` @@ -256,7 +273,13 @@ function scoreSoftFactor(key: SoftFactorKey, weight: number, property: Property) case 'expansionPotential': return sf?.expansionPotentialScore case 'flexibility': return sf?.flexibilityScore case 'visibility': return sf?.visibilityScore - case 'footfall': return sf?.footfallScore + case 'footfall': { + if (sf?.footfallScore !== undefined) return sf.footfallScore + // Map passerbyFrequency string (RETAIL properties) to numeric score + const pfMap: Record = { HIGH: 85, MEDIUM_HIGH: 68, MEDIUM: 50, LOW: 30 } + const pf = (sf as { passerbyFrequency?: string } | undefined)?.passerbyFrequency + return pf !== undefined ? (pfMap[pf] ?? 50) : undefined + } case 'talentAccess': return sf?.talentAccessScore ?? sf?.talentAccess case 'esg': return sf?.esgScore case 'taxEnvironment': return sf?.taxEnvironmentScore @@ -292,8 +315,10 @@ function scoreSoftFactor(key: SoftFactorKey, weight: number, property: Property) } } - // Soft factor values are 0–1 scale → convert to 0–100 - const score = Math.round(Math.min(100, Math.max(0, rawValue * 100))) + // Support both 0–1 float scale (enrichment estimates) and 0–100 integer scale (mock data) + const score = rawValue > 1 + ? Math.round(Math.min(100, Math.max(0, rawValue))) + : Math.round(Math.min(100, Math.max(0, rawValue * 100))) const LABELS: Record = { prestige: 'Prestige', accessibility: 'Erreichbarkeit', expansionPotential: 'Expansion', flexibility: 'Flexibilität', visibility: 'Sichtbarkeit', footfall: 'Passantenfrequenz', @@ -308,6 +333,157 @@ function scoreSoftFactor(key: SoftFactorKey, weight: number, property: Property) } } +// ── Structured Requirement Scorers ─────────────────────────────────────────── + +const FIT_OUT_LEVELS: Record = { SHELL: 0, BASIC: 1, FULL: 2, PREMIUM: 3 } + +function scoreGroundFloor(need: Need, property: Property): ScoreFactor | null { + if (!need.requireGroundFloor) return null + const floor = property.hardFacts?.floor ?? property.floorLevel + if (floor === undefined) { + return { criterion: 'groundFloor', weight: 0.05, score: 50, contribution: 2.5, explanation: 'Erdgeschoss erforderlich — Stockwerk nicht dokumentiert', estimated: true } + } + const passed = floor === 0 + const score = passed ? 100 : 20 + return { + criterion: 'groundFloor', + weight: 0.05, + score, + contribution: score * 0.05, + explanation: passed ? `Erdgeschoss bestätigt (Etage ${floor})` : `Nicht Erdgeschoss (Etage ${floor}) — EG erforderlich`, + } +} + +function scoreParkingMin(need: Need, property: Property): ScoreFactor | null { + const min = need.requiredParkingMin + if (!min || min <= 0) return null + const available = property.hardFacts?.parking ?? property.softFactors?.parkingSpots ?? 0 + if (available === 0 && property.hardFacts?.parking === undefined) { + return { criterion: 'parkingMin', weight: 0.04, score: 50, contribution: 2, explanation: `Mind. ${min} Parkplätze erforderlich — keine Daten`, estimated: true } + } + const ratio = Math.min(1, available / min) + const score = available >= min ? 100 : Math.round(ratio * 60) + return { + criterion: 'parkingMin', + weight: 0.04, + score, + contribution: score * 0.04, + explanation: available >= min + ? `${available} Parkplätze vorhanden (mind. ${min} erforderlich)` + : `Nur ${available} Parkplätze (mind. ${min} erforderlich)`, + } +} + +function scoreAirConditioning(need: Need, property: Property): ScoreFactor | null { + if (!need.requireAirConditioning) return null + const hasAC = property.hardFacts?.hasAirConditioning + if (hasAC === undefined) { + return { criterion: 'airConditioning', weight: 0.03, score: 50, contribution: 1.5, explanation: 'Klimaanlage erforderlich — nicht dokumentiert', estimated: true } + } + const score = hasAC ? 100 : 15 + return { + criterion: 'airConditioning', + weight: 0.03, + score, + contribution: score * 0.03, + explanation: hasAC ? 'Klimaanlage vorhanden' : 'Keine Klimaanlage — Klimaanlage erforderlich', + } +} + +function scoreLoadingDock(need: Need, property: Property): ScoreFactor | null { + if (!need.requireLoadingDock) return null + const docks = property.hardFacts?.loadingDocksCount + if (docks === undefined) { + return { criterion: 'loadingDock', weight: 0.05, score: 50, contribution: 2.5, explanation: 'Laderampe erforderlich — keine Daten', estimated: true } + } + const passed = docks > 0 + const score = passed ? 100 : 15 + return { + criterion: 'loadingDock', + weight: 0.05, + score, + contribution: score * 0.05, + explanation: passed ? `${docks} Laderampe(n) vorhanden` : 'Keine Laderampe vorhanden — erforderlich', + } +} + +function scoreBarrierFree(need: Need, property: Property): ScoreFactor | null { + if (!need.requireBarrierFree) return null + const ok = property.hardFacts?.isBarrierFree + if (ok === undefined) { + return { criterion: 'barrierFree', weight: 0.03, score: 50, contribution: 1.5, explanation: 'Barrierefreiheit erforderlich — nicht dokumentiert', estimated: true } + } + const score = ok ? 100 : 20 + return { + criterion: 'barrierFree', + weight: 0.03, + score, + contribution: score * 0.03, + explanation: ok ? 'Barrierefrei bestätigt' : 'Nicht barrierefrei — Barrierefreiheit erforderlich', + } +} + +function scoreCeilingHeight(need: Need, property: Property): ScoreFactor | null { + const min = need.minCeilingHeightM + if (!min || min <= 0) return null + const actual = property.hardFacts?.ceilingHeightM + if (actual === undefined) { + return { criterion: 'ceilingHeight', weight: 0.04, score: 50, contribution: 2, explanation: `Mind. ${min}m Deckenhöhe erforderlich — keine Daten`, estimated: true } + } + const passed = actual >= min + const score = passed ? 100 : Math.max(10, Math.round((actual / min) * 70)) + return { + criterion: 'ceilingHeight', + weight: 0.04, + score, + contribution: score * 0.04, + explanation: passed + ? `Deckenhöhe ${actual}m ≥ Minimum ${min}m` + : `Deckenhöhe ${actual}m unter Minimum ${min}m`, + } +} + +function scoreMinContractDuration(need: Need, property: Property): ScoreFactor | null { + const min = need.minContractDurationMonths + if (!min || min <= 0) return null + const available = property.contractDurationMonths + if (available === undefined) { + return { criterion: 'contractDuration', weight: 0.03, score: 50, contribution: 1.5, explanation: `Mind. ${Math.round(min / 12)} Jahre Laufzeit gewünscht — keine Daten`, estimated: true } + } + const passed = available >= min + const score = passed ? 100 : Math.max(20, Math.round((available / min) * 70)) + return { + criterion: 'contractDuration', + weight: 0.03, + score, + contribution: score * 0.03, + explanation: passed + ? `${Math.round(available / 12)} Jahre Vertragslaufzeit — erfüllt (mind. ${Math.round(min / 12)} Jahre gewünscht)` + : `Nur ${Math.round(available / 12)} Jahre — mind. ${Math.round(min / 12)} Jahre gewünscht`, + } +} + +function scoreFitOut(need: Need, property: Property): ScoreFactor | null { + if (!need.requiredFitOut) return null + const propFitOut = property.hardFacts?.fitOut + if (!propFitOut) { + return { criterion: 'fitOut', weight: 0.04, score: 50, contribution: 2, explanation: `Ausbaustandard ${need.requiredFitOut} erforderlich — keine Daten`, estimated: true } + } + const reqLevel = FIT_OUT_LEVELS[need.requiredFitOut] ?? 1 + const propLevel = FIT_OUT_LEVELS[propFitOut] ?? 0 + const score = propLevel >= reqLevel ? 100 : Math.max(10, Math.round(50 - (reqLevel - propLevel) * 25)) + const LABELS: Record = { SHELL: 'Rohbau', BASIC: 'Basisausbau', FULL: 'Vollausbau', PREMIUM: 'Premiumausbau' } + return { + criterion: 'fitOut', + weight: 0.04, + score, + contribution: score * 0.04, + explanation: propLevel >= reqLevel + ? `Ausbaustandard ${LABELS[propFitOut] ?? propFitOut} erfüllt Anforderung ${LABELS[need.requiredFitOut] ?? need.requiredFitOut}` + : `${LABELS[propFitOut] ?? propFitOut} — ${LABELS[need.requiredFitOut] ?? need.requiredFitOut} erforderlich`, + } +} + // ── Modifier Calculators ────────────────────────────────────────────────────── export function calcDataQualityModifier(property: Property): number { @@ -369,13 +545,28 @@ export function calculateScore(need: Need, property: Property): MatchEngineOutpu const profile = resolveProfile(need, property) // ── Hard criteria scoring ────────────────────────────────────────────────── - const hardFactors: ScoreFactor[] = [ + const coreHardFactors: ScoreFactor[] = [ scoreArea(need, property, profile.area), scoreLocation(need, property, profile.location), scoreBudget(need, property, profile.budget), scoreTiming(need, property, profile.timing), ] + + // Structured requirement scorers — optional hard factors + const structuredFactors: ScoreFactor[] = [ + scoreGroundFloor(need, property), + scoreParkingMin(need, property), + scoreAirConditioning(need, property), + scoreLoadingDock(need, property), + scoreBarrierFree(need, property), + scoreCeilingHeight(need, property), + scoreMinContractDuration(need, property), + scoreFitOut(need, property), + ].filter((f): f is ScoreFactor => f !== null) + + const hardFactors = [...coreHardFactors, ...structuredFactors] const hardWeightSum = HARD_CRITERION_KEYS.reduce((s, k) => s + profile[k], 0) + + structuredFactors.reduce((s, f) => s + f.weight, 0) const hardRaw = hardFactors.reduce((s, f) => s + f.contribution, 0) const hardMatchScore = hardWeightSum > 0 ? Math.min(100, Math.round(hardRaw / hardWeightSum)) : 0 @@ -388,14 +579,21 @@ export function calculateScore(need: Need, property: Property): MatchEngineOutpu const softRaw = allSoftDisplay.filter(f => f.weight > 0).reduce((s, f) => s + f.contribution, 0) const softFactorScore = softWeightSum > 0 ? Math.min(100, Math.round(softRaw / softWeightSum)) : 50 + // ── Must-have criteria evaluation ───────────────────────────────────────── + const allMustHaveText = [ + ...(need.mustCriteriaText ?? []), + ...(need.mustHaveCriteria?.map(c => c.criterion) ?? []), + ] + const mustHaveEval = scoreMustHaves(allMustHaveText, property) + // ── Modifiers ────────────────────────────────────────────────────────────── const dqMod = calcDataQualityModifier(property) const confMod = calcConfidenceModifier(property) - // ── Final score: weighted sum of both groups + modifiers ────────────────── + // ── Final score: weighted sum of both groups + modifiers + must-have penalty // Each group already normalized 0–100; combine per SCORE_SPLIT, then apply modifiers const baseScore = hardMatchScore * 0.60 + softFactorScore * 0.40 - const rawFinal = baseScore + dqMod + confMod - hardFilter.severePenalty + const rawFinal = baseScore + dqMod + confMod - hardFilter.severePenalty + mustHaveEval.scoreImpact const finalScore = Math.round(Math.min(100, Math.max(0, rawFinal))) // ── Factor classification — only use weighted soft factors for positive/negative ── @@ -432,6 +630,7 @@ export function calculateScore(need: Need, property: Property): MatchEngineOutpu tradeOffs, risks, missingData, + mustHaveEvaluation: mustHaveEval.results.length > 0 ? mustHaveEval.results : undefined, nextBestActions: [], // filled by rankingEngine } diff --git a/src/mock-data/needs.ts b/src/mock-data/needs.ts index 4140dfb..3a88370 100644 --- a/src/mock-data/needs.ts +++ b/src/mock-data/needs.ts @@ -368,4 +368,108 @@ export const mockNeeds: Need[] = [ createdAt: '2025-04-10T08:00:00Z', updatedAt: '2025-05-06T13:00:00Z', }, + + // --- need-uc-a: Renato's Fahrradhändler — Use Case A --- + { + id: 'need-uc-a', + companyName: 'Velo City GmbH', + contactName: 'Renato Marchetti', + assetType: AssetType.RETAIL, + requiredArea: { min: 120, max: 160 }, + preferredLocations: ['Zürich Kreis 3', 'Zürich Kreis 4', 'Zürich Kreis 5', 'Zürich Kreis 8', 'Wädenswil', 'Thalwil', 'Uster'], + budgetRange: { maxPerSqm: 620, currency: 'CHF' }, + timing: { + earliestMoveIn: '2026-10-01', + latestMoveIn: '2027-03-31', + contractDurationMonths: 60, + flexibleTiming: true, + }, + mustCriteriaText: ['Schaufenster', 'Erdgeschoss', 'Werkstatt möglich'], + requireGroundFloor: true, + requiredFitOut: 'FULL', + notes: 'Wunsch: 3.5-Zi-Wohnung im Haus oder in der Nähe, max. CHF 2\'200/Mt. inkl. NK', + softFactors: { + requireHighVisibility: true, + }, + weightingProfile: { + area: 0.13, location: 0.20, budget: 0.14, timing: 0.12, + prestige: 0.04, accessibility: 0.07, expansionPotential: 0.02, flexibility: 0.04, + visibility: 0.12, footfall: 0.12, talentAccess: 0.01, esg: 0.01, taxEnvironment: 0.00, + }, + confidenceInCriteria: 0.90, + extractedFromText: 'Fahrradhändler sucht Verkaufslokal 120–150m² EG mit Schaufenster und Werkstatt in Zürich Kreis 3/4/5/8 oder Wädenswil/Thalwil/Uster, bezugsfertig Q4/2026 oder Q1/2027.', + organizationId: 'org-wincasa', + createdAt: '2025-05-20T10:00:00Z', + updatedAt: '2025-05-22T10:00:00Z', + }, + + // --- need-uc-b: Renato's Umzugsfirma — Use Case B --- + { + id: 'need-uc-b', + companyName: 'Alp Transit Umzüge GmbH', + contactName: 'Renato Marchetti', + assetType: AssetType.OFFICE, + requiredArea: { min: 140, max: 200 }, + preferredLocations: ['Zürich', 'Zürich-West', 'Zürich Kreis 5', 'Zürich Kreis 4'], + budgetRange: { maxPerSqm: 300, currency: 'CHF' }, + timing: { + earliestMoveIn: '2025-10-01', + latestMoveIn: '2026-06-01', + contractDurationMonths: 84, + flexibleTiming: false, + }, + mustCriteriaText: ['3-5 Parkplätze', 'Verpflegungsmöglichkeit', 'Startup-Umfeld', 'Erweiterungsoption vorhanden', 'Repräsentativer Charakter'], + requiredParkingMin: 3, + requiredFitOut: 'BASIC', + minContractDurationMonths: 84, + softFactors: { + requireParking: true, + }, + weightingProfile: { + area: 0.14, location: 0.17, budget: 0.22, timing: 0.11, + prestige: 0.04, accessibility: 0.08, expansionPotential: 0.07, flexibility: 0.07, + visibility: 0.02, footfall: 0.01, talentAccess: 0.07, esg: 0.02, taxEnvironment: 0.01, + }, + confidenceInCriteria: 0.88, + extractedFromText: 'Büro 140–200m², Zürich-West, max. CHF 300/m²/a, einfacher Ausbau, 3-5 Parkplätze, 7 Jahre Mietvertrag, Startup-Umfeld, Erweiterungsoption.', + organizationId: 'org-wincasa', + createdAt: '2025-05-22T10:00:00Z', + updatedAt: '2025-05-22T10:00:00Z', + }, + + // --- need-uc-c: Renato's Vermögensverwalter — Use Case C --- + { + id: 'need-uc-c', + companyName: 'Wealth Advisory Partners AG', + contactName: 'Renato Marchetti', + assetType: AssetType.OFFICE, + requiredArea: { min: 450, max: 560 }, + preferredLocations: ['Zürich', 'Zürich Kreis 1', 'Zürich Seefeld', 'Zürich Innenstadt'], + budgetRange: { maxPerSqm: 900, currency: 'CHF' }, + timing: { + earliestMoveIn: '2026-01-01', + latestMoveIn: '2027-06-01', + contractDurationMonths: 120, + flexibleTiming: false, + }, + mustCriteriaText: ['Klimaanlage', 'Tageslicht', 'Prestige-Adresse', 'Repräsentativer Eingang', '2×5-Jahre-Mietoption'], + notes: 'Mietvertrag min. 10 Jahre + 2×5 Jahre echte Optionen gefordert', + requireAirConditioning: true, + requiredFitOut: 'PREMIUM', + minContractDurationMonths: 120, + softFactors: { + minPrestige: 85, + minAccessibility: 80, + }, + weightingProfile: { + area: 0.10, location: 0.25, budget: 0.09, timing: 0.08, + prestige: 0.20, accessibility: 0.10, expansionPotential: 0.02, flexibility: 0.02, + visibility: 0.04, footfall: 0.01, talentAccess: 0.06, esg: 0.03, taxEnvironment: 0.05, + }, + confidenceInCriteria: 0.95, + extractedFromText: 'Vermögensverwalter sucht repräsentative Bürofläche 500m² in Zürich Kreis 1 oder Seefeld/Bellevue, Klimaanlage, PREMIUM-Ausbau, 10-Jahres-Mietvertrag.', + organizationId: 'org-wincasa', + createdAt: '2025-05-20T10:00:00Z', + updatedAt: '2025-05-22T10:00:00Z', + }, ] diff --git a/src/mock-data/properties.ts b/src/mock-data/properties.ts index ec47dd2..27d9740 100644 --- a/src/mock-data/properties.ts +++ b/src/mock-data/properties.ts @@ -37,6 +37,7 @@ export const mockProperties: Property[] = [ parkingSpots: 12, publicTransportMinutes: 4, }, + hardFacts: { floor: 3, parking: 12, hasAirConditioning: true, fitOut: 'FULL', ceilingHeightM: 2.8 }, floorLevel: 3, contractDurationMonths: 60, ancillaryCosts: 5.5, @@ -93,6 +94,7 @@ export const mockProperties: Property[] = [ parkingSpots: 30, publicTransportMinutes: 12, }, + hardFacts: { floor: 0, parking: 30, loadingDocksCount: 2, ceilingHeightM: 7.5, isBarrierFree: true }, floorLevel: 0, expansionPotentialSqm: 800, contractDurationMonths: 36, @@ -265,6 +267,7 @@ export const mockProperties: Property[] = [ parkingSpots: 25, publicTransportMinutes: 14, }, + hardFacts: { floor: 0, parking: 25, loadingDocksCount: 4, ceilingHeightM: 8, isBarrierFree: true }, floorLevel: 0, expansionPotentialSqm: 600, contractDurationMonths: 60, @@ -321,6 +324,7 @@ export const mockProperties: Property[] = [ accessibility: 96, publicTransportMinutes: 2, }, + hardFacts: { floor: 0, parking: 0, hasStorefront: true, hasAirConditioning: true, fitOut: 'PREMIUM', isBarrierFree: true }, floorLevel: 0, contractDurationMonths: 60, ancillaryCosts: 8.0, @@ -575,6 +579,497 @@ export const mockProperties: Property[] = [ updatedAt: '2025-05-04T09:00:00Z', }, + // ── Use Case A: Fahrradhändler — Retail Zürich Kreis 3 (Wiedikon) ────────────── + + { + id: 'prop-046', + title: 'Ladenlokal Birmensdorferstrasse 108', + assetType: AssetType.RETAIL, + resultType: ResultType.VERIFIED_PORTFOLIO, + location: { city: 'Zürich', district: 'Kreis 3', canton: 'ZH', country: 'CH', coordinates: { lat: 47.3697, lng: 8.5205 } }, + address: { street: 'Birmensdorferstrasse', houseNumber: '108', postalCode: '8003', city: 'Zürich', country: 'CH' }, + areaSqm: 130, + rentPricePerSqm: 520, + totalRentMonthly: 67600, + availabilityDate: '2026-10-01', + availabilityStatus: AvailabilityStatus.AVAILABLE_SOON, + sourceType: 'ERP_IMPORT', + confidenceScore: 0.95, + dataQuality: { + score: 0.92, + missingCriticalFields: [], + missingOptionalFields: [], + lastVerifiedAt: '2025-05-20', + freshness: DataFreshness.FRESH, + warnings: [], + }, + softFactors: { + prestige: 56, + accessibility: 88, + visibilityScore: 72, + passerbyFrequency: 'MEDIUM_HIGH', + talentAccess: 65, + parkingSpots: 1, + publicTransportMinutes: 5, + }, + hardFacts: { floor: 0, parking: 1, hasStorefront: true, fitOut: 'FULL', isBarrierFree: true }, + floorLevel: 0, + contractDurationMonths: 60, + ancillaryCosts: 6.0, + riskLevel: RiskLevel.LOW, + images: ['https://images.unsplash.com/photo-1567538096630-e0c55bd6374c?w=800&h=400&fit=crop'], + mapImageUrl: 'https://images.unsplash.com/photo-1524661135-423995f22d0b?w=600&h=300&fit=crop', + propertyNumber: 'ZH-2024-046', + description: 'Vollausgebautes Ladenlokal an der Birmensdorferstrasse, Zürich Kreis 3 (Wiedikon). Schaufensterfront EG, Rückraum (20 m²) als Werkstatt oder Lager nutzbar. 3-Zimmer-Wohnung im gleichen Gebäude per Oktober 2026 mitbezugsbereit. Quartierstrasse mit stabiler Passantenfrequenz, ÖV 5 Min. (Tram 9/14). Verfügbar Q4/2026.', + units: [ + { id: 'unit-046-1', propertyId: 'prop-046', floorLevel: 0, unitLabel: 'EG Laden', areaSqm: 110, available: true, rentPricePerSqm: 520 }, + { id: 'unit-046-2', propertyId: 'prop-046', floorLevel: 0, unitLabel: 'EG Werkstattraum', areaSqm: 20, available: true, rentPricePerSqm: 320 }, + ], + importedFrom: 'SAP RE-FX', + importedAt: '2025-01-15T08:00:00Z', + lastUpdatedAt: '2025-05-20T10:00:00Z', + breakoutOption: false, + schattenmarktRelease: { enabled: false }, + organizationId: 'org-wincasa', + createdAt: '2025-05-22T10:00:00Z', + updatedAt: '2025-05-22T10:00:00Z', + }, + + // ── Use Case A: Fahrradhändler — Retail Zürich Kreis 8 (Riesbach) ─────────────── + + { + id: 'prop-047', + title: 'Ladenlokal Seefeldstrasse 62', + assetType: AssetType.RETAIL, + resultType: ResultType.VERIFIED_PORTFOLIO, + location: { city: 'Zürich', district: 'Kreis 8', canton: 'ZH', country: 'CH', coordinates: { lat: 47.3545, lng: 8.5492 } }, + address: { street: 'Seefeldstrasse', houseNumber: '62', postalCode: '8008', city: 'Zürich', country: 'CH' }, + areaSqm: 142, + rentPricePerSqm: 590, + totalRentMonthly: 83780, + availabilityDate: '2026-11-01', + availabilityStatus: AvailabilityStatus.AVAILABLE_SOON, + sourceType: 'ERP_IMPORT', + confidenceScore: 0.94, + dataQuality: { + score: 0.91, + missingCriticalFields: [], + missingOptionalFields: [], + lastVerifiedAt: '2025-05-20', + freshness: DataFreshness.FRESH, + warnings: [], + }, + softFactors: { + prestige: 72, + accessibility: 90, + visibilityScore: 78, + passerbyFrequency: 'HIGH', + talentAccess: 76, + parkingSpots: 2, + publicTransportMinutes: 4, + }, + hardFacts: { floor: 0, parking: 2, hasStorefront: true, fitOut: 'FULL', isBarrierFree: true }, + floorLevel: 0, + contractDurationMonths: 60, + ancillaryCosts: 6.5, + riskLevel: RiskLevel.LOW, + images: ['https://images.unsplash.com/photo-1472851294608-062f824d29cc?w=800&h=400&fit=crop'], + mapImageUrl: 'https://images.unsplash.com/photo-1524661135-423995f22d0b?w=600&h=300&fit=crop', + propertyNumber: 'ZH-2024-047', + description: 'Bezugsfertiges Ladenlokal in Zürich Kreis 8 (Riesbach/Seefeld), Seefeldstrasse. Grosszügige Schaufensterfront, EG, vollausgebaut. Hinterraum (22 m²) ideal für Werkstatt oder Lager. Gehobenes Quartier mit hohem Kundenpotenzial, nahe Zürichsee. 2 Parkplätze, Veloinfrastruktur, ÖV 4 Min. Verfügbar November 2026.', + units: [ + { id: 'unit-047-1', propertyId: 'prop-047', floorLevel: 0, unitLabel: 'EG Laden', areaSqm: 120, available: true, rentPricePerSqm: 590 }, + { id: 'unit-047-2', propertyId: 'prop-047', floorLevel: 0, unitLabel: 'EG Nebenraum', areaSqm: 22, available: true, rentPricePerSqm: 380 }, + ], + importedFrom: 'SAP RE-FX', + importedAt: '2025-01-15T08:00:00Z', + lastUpdatedAt: '2025-05-20T10:00:00Z', + breakoutOption: false, + schattenmarktRelease: { enabled: false }, + organizationId: 'org-wincasa', + createdAt: '2025-05-22T10:00:00Z', + updatedAt: '2025-05-22T10:00:00Z', + }, + + // ── Use Case A: Fahrradhändler — Retail Wädenswil (Seegemeinde) ────────────────── + + { + id: 'prop-048', + title: 'Ladenlokal Seestrasse 44, Wädenswil', + assetType: AssetType.RETAIL, + resultType: ResultType.VERIFIED_PORTFOLIO, + location: { city: 'Wädenswil', district: 'Zentrum', canton: 'ZH', country: 'CH', coordinates: { lat: 47.2260, lng: 8.6778 } }, + address: { street: 'Seestrasse', houseNumber: '44', postalCode: '8820', city: 'Wädenswil', country: 'CH' }, + areaSqm: 125, + rentPricePerSqm: 380, + totalRentMonthly: 47500, + availabilityDate: '2026-10-01', + availabilityStatus: AvailabilityStatus.AVAILABLE_SOON, + sourceType: 'ERP_IMPORT', + confidenceScore: 0.93, + dataQuality: { + score: 0.90, + missingCriticalFields: [], + missingOptionalFields: [], + lastVerifiedAt: '2025-05-20', + freshness: DataFreshness.FRESH, + warnings: [], + }, + softFactors: { + prestige: 48, + accessibility: 82, + visibilityScore: 65, + passerbyFrequency: 'MEDIUM', + talentAccess: 52, + parkingSpots: 4, + publicTransportMinutes: 8, + }, + hardFacts: { floor: 0, parking: 4, hasStorefront: true, fitOut: 'FULL', isBarrierFree: true }, + floorLevel: 0, + contractDurationMonths: 60, + ancillaryCosts: 5.5, + riskLevel: RiskLevel.LOW, + images: ['https://images.unsplash.com/photo-1555529669-e69e7aa0ba9a?w=800&h=400&fit=crop'], + mapImageUrl: 'https://images.unsplash.com/photo-1524661135-423995f22d0b?w=600&h=300&fit=crop', + propertyNumber: 'WAE-2024-048', + description: 'Vollausgebautes Ladenlokal im Zentrum von Wädenswil, direkt an der Seestrasse. Schaufensterfront EG, Werkstattraum im Hinterhaus (25 m²). 4 Parkplätze im Innenhof. Bahnhof Wädenswil (S-Bahn) in 8 Min. zu Fuss — ideale Lage für Fahrradhandel und -service in der Seegemeinde. Verfügbar Q4/2026.', + units: [ + { id: 'unit-048-1', propertyId: 'prop-048', floorLevel: 0, unitLabel: 'EG Laden', areaSqm: 100, available: true, rentPricePerSqm: 380 }, + { id: 'unit-048-2', propertyId: 'prop-048', floorLevel: 0, unitLabel: 'EG Werkstatt', areaSqm: 25, available: true, rentPricePerSqm: 240 }, + ], + importedFrom: 'SAP RE-FX', + importedAt: '2025-01-15T08:00:00Z', + lastUpdatedAt: '2025-05-20T10:00:00Z', + breakoutOption: false, + schattenmarktRelease: { enabled: false }, + organizationId: 'org-wincasa', + createdAt: '2025-05-22T10:00:00Z', + updatedAt: '2025-05-22T10:00:00Z', + }, + + // ── Use Case A: Fahrradhändler — Retail Zürich Kreis 5 (Langstrasse) ────────── + + { + id: 'prop-040', + title: 'Ladenlokal Langstrasse 84', + assetType: AssetType.RETAIL, + resultType: ResultType.VERIFIED_PORTFOLIO, + location: { city: 'Zürich', district: 'Kreis 5', canton: 'ZH', country: 'CH', coordinates: { lat: 47.3785, lng: 8.5268 } }, + address: { street: 'Langstrasse', houseNumber: '84', postalCode: '8004', city: 'Zürich', country: 'CH' }, + areaSqm: 135, + rentPricePerSqm: 570, + totalRentMonthly: 76950, + availabilityDate: '2026-11-01', + availabilityStatus: AvailabilityStatus.AVAILABLE_SOON, + sourceType: 'ERP_IMPORT', + confidenceScore: 0.96, + dataQuality: { + score: 0.93, + missingCriticalFields: [], + missingOptionalFields: [], + lastVerifiedAt: '2025-05-15', + freshness: DataFreshness.FRESH, + warnings: [], + }, + softFactors: { + prestige: 62, + accessibility: 90, + visibilityScore: 80, + passerbyFrequency: 'HIGH', + talentAccess: 72, + parkingSpots: 0, + publicTransportMinutes: 4, + }, + hardFacts: { floor: 0, parking: 0, hasStorefront: true, fitOut: 'FULL', isBarrierFree: true }, + floorLevel: 0, + contractDurationMonths: 60, + ancillaryCosts: 6.5, + riskLevel: RiskLevel.LOW, + images: ['https://images.unsplash.com/photo-1441986300917-64674bd600d8?w=800&h=400&fit=crop'], + mapImageUrl: 'https://images.unsplash.com/photo-1524661135-423995f22d0b?w=600&h=300&fit=crop', + propertyNumber: 'ZH-2024-040', + description: 'Bezugsfertiges Ladenlokal an der Langstrasse, Zürich Kreis 5. Grosszügige Schaufensterfront, EG, vollständig ausgebaut. Nebenraum (28 m²) als Werkstatt oder Lager nutzbar. Lebhafter Standort mit hohem Fussgängeraufkommen. Verfügbar November 2026. Kein Parkplatz direkt, Velounterstand vorhanden.', + units: [ + { id: 'unit-040-1', propertyId: 'prop-040', floorLevel: 0, unitLabel: 'EG Laden', areaSqm: 107, available: true, rentPricePerSqm: 570 }, + { id: 'unit-040-2', propertyId: 'prop-040', floorLevel: 0, unitLabel: 'EG Nebenraum', areaSqm: 28, available: true, rentPricePerSqm: 360 }, + ], + importedFrom: 'SAP RE-FX', + importedAt: '2025-01-15T08:00:00Z', + lastUpdatedAt: '2025-05-15T10:00:00Z', + breakoutOption: false, + schattenmarktRelease: { enabled: false }, + organizationId: 'org-wincasa', + createdAt: '2025-05-22T10:00:00Z', + updatedAt: '2025-05-22T10:00:00Z', + }, + + { + id: 'prop-041', + title: 'Ladenlokal Stauffacherstrasse 42', + assetType: AssetType.RETAIL, + resultType: ResultType.VERIFIED_PORTFOLIO, + location: { city: 'Zürich', district: 'Kreis 4', canton: 'ZH', country: 'CH', coordinates: { lat: 47.3728, lng: 8.5292 } }, + address: { street: 'Stauffacherstrasse', houseNumber: '42', postalCode: '8004', city: 'Zürich', country: 'CH' }, + areaSqm: 128, + rentPricePerSqm: 540, + totalRentMonthly: 69120, + availabilityDate: '2027-02-01', + availabilityStatus: AvailabilityStatus.AVAILABLE_SOON, + sourceType: 'ERP_IMPORT', + confidenceScore: 0.95, + dataQuality: { + score: 0.92, + missingCriticalFields: [], + missingOptionalFields: [], + lastVerifiedAt: '2025-05-15', + freshness: DataFreshness.FRESH, + warnings: [], + }, + softFactors: { + prestige: 60, + accessibility: 88, + visibilityScore: 75, + passerbyFrequency: 'MEDIUM_HIGH', + talentAccess: 70, + parkingSpots: 2, + publicTransportMinutes: 5, + }, + hardFacts: { floor: 0, parking: 2, hasStorefront: true, fitOut: 'FULL', isBarrierFree: true }, + floorLevel: 0, + contractDurationMonths: 60, + ancillaryCosts: 6.0, + riskLevel: RiskLevel.LOW, + images: ['https://images.unsplash.com/photo-1555529669-e69e7aa0ba9a?w=800&h=400&fit=crop'], + mapImageUrl: 'https://images.unsplash.com/photo-1524661135-423995f22d0b?w=600&h=300&fit=crop', + propertyNumber: 'ZH-2024-041', + description: 'Vollausgebautes Ladenlokal am Helvetiaplatz, Zürich Kreis 4. EG mit Schaufensterfront, Hinterraum für Werkstatt oder Lager. Kleines Lager im UG optional. Standort mit guter Passantenfrequenz, 2 Parkplätze. Verfügbar Februar 2027 — ideal für Bezug Q1/27.', + units: [ + { id: 'unit-041-1', propertyId: 'prop-041', floorLevel: 0, unitLabel: 'EG Laden', areaSqm: 128, available: true, rentPricePerSqm: 540 }, + ], + importedFrom: 'SAP RE-FX', + importedAt: '2025-01-15T08:00:00Z', + lastUpdatedAt: '2025-05-15T10:00:00Z', + breakoutOption: false, + schattenmarktRelease: { enabled: false }, + organizationId: 'org-wincasa', + createdAt: '2025-05-22T10:00:00Z', + updatedAt: '2025-05-22T10:00:00Z', + }, + + // ── Use Case B: Umzugsfirma — Office Zürich-West Industrie Loft ─────────────── + + { + id: 'prop-042', + title: 'Bürofläche Pfingstweidstrasse 6', + assetType: AssetType.OFFICE, + resultType: ResultType.VERIFIED_PORTFOLIO, + location: { city: 'Zürich', district: 'Zürich-West', canton: 'ZH', country: 'CH', coordinates: { lat: 47.3878, lng: 8.5080 } }, + address: { street: 'Pfingstweidstrasse', houseNumber: '6', postalCode: '8005', city: 'Zürich', country: 'CH' }, + areaSqm: 162, + rentPricePerSqm: 268, + totalRentMonthly: 43416, + availabilityDate: '2025-10-01', + availabilityStatus: AvailabilityStatus.AVAILABLE_SOON, + sourceType: 'ERP_IMPORT', + confidenceScore: 0.96, + dataQuality: { + score: 0.93, + missingCriticalFields: [], + missingOptionalFields: [], + lastVerifiedAt: '2025-05-20', + freshness: DataFreshness.FRESH, + warnings: [], + }, + softFactors: { + prestige: 55, + accessibility: 84, + visibilityScore: 50, + talentAccess: 80, + parkingSpots: 4, + publicTransportMinutes: 6, + }, + hardFacts: { floor: 1, parking: 4, fitOut: 'BASIC', ceilingHeightM: 3.2 }, + floorLevel: 1, + expansionPotentialSqm: 90, + contractDurationMonths: 84, + ancillaryCosts: 3.8, + riskLevel: RiskLevel.LOW, + images: ['https://images.unsplash.com/photo-1497366216548-37526070297c?w=800&h=400&fit=crop'], + mapImageUrl: 'https://images.unsplash.com/photo-1524661135-423995f22d0b?w=600&h=300&fit=crop', + propertyNumber: 'ZH-2023-042', + description: 'Loft-Bürofläche mit Industrie-Charakter, Pfingstweidstrasse 6, Zürich-West. 162 m², 1. OG in einem ehemaligen Fabrikgebäude — hohe Decken (3.2 m), Sichtbeton, natürliches Licht. Raum für 10–12 Arbeitsplätze + 2 Sitzungszimmer möglich. 4 Parkplätze im Hof, Gemeinschaftskantine im EG. Umfeld: dynamisches Startup-Ökosystem, junge Unternehmen und Kreativagenturen im Haus. Mietvertrag 7 Jahre bevorzugt, Erweiterungsoption 90 m² verfügbar.', + units: [ + { id: 'unit-042-1', propertyId: 'prop-042', floorLevel: 1, unitLabel: '1.OG West', areaSqm: 162, available: true, rentPricePerSqm: 268 }, + ], + importedFrom: 'SAP RE-FX', + importedAt: '2025-01-15T08:00:00Z', + lastUpdatedAt: '2025-05-20T10:00:00Z', + breakoutOption: false, + schattenmarktRelease: { enabled: false }, + organizationId: 'org-wincasa', + createdAt: '2025-05-22T10:00:00Z', + updatedAt: '2025-05-22T10:00:00Z', + }, + + // ── Use Case C: Vermögensverwalter — Premium Office Zürich Kreis 1 / Seefeld ── + + { + id: 'prop-043', + title: 'Bürofläche Bahnhofstrasse 52', + assetType: AssetType.OFFICE, + resultType: ResultType.VERIFIED_PORTFOLIO, + location: { city: 'Zürich', district: 'Kreis 1', canton: 'ZH', country: 'CH', coordinates: { lat: 47.3712, lng: 8.5382 } }, + address: { street: 'Bahnhofstrasse', houseNumber: '52', postalCode: '8001', city: 'Zürich', country: 'CH' }, + areaSqm: 515, + rentPricePerSqm: 840, + totalRentMonthly: 432600, + availabilityDate: '2026-04-01', + availabilityStatus: AvailabilityStatus.AVAILABLE_SOON, + sourceType: 'ERP_IMPORT', + confidenceScore: 0.98, + dataQuality: { + score: 0.96, + missingCriticalFields: [], + missingOptionalFields: [], + lastVerifiedAt: '2025-05-20', + freshness: DataFreshness.FRESH, + warnings: [], + }, + softFactors: { + prestige: 98, + accessibility: 98, + visibilityScore: 92, + talentAccess: 92, + parkingSpots: 6, + publicTransportMinutes: 2, + }, + hardFacts: { floor: 6, parking: 6, hasAirConditioning: true, fitOut: 'PREMIUM', ceilingHeightM: 3.0, isBarrierFree: true }, + floorLevel: 6, + contractDurationMonths: 120, + ancillaryCosts: 9.0, + riskLevel: RiskLevel.LOW, + images: ['https://images.unsplash.com/photo-1560472355-536de3962603?w=800&h=400&fit=crop'], + mapImageUrl: 'https://images.unsplash.com/photo-1524661135-423995f22d0b?w=600&h=300&fit=crop', + propertyNumber: 'ZH-2022-043', + description: 'Exklusive Büroetage an der Bahnhofstrasse, Zürich Kreis 1 — eine der prestigiösesten Adressen der Schweiz. 515 m² im 6. Obergeschoss mit Panoramablick über die Innenstadt. PREMIUM-Ausbau, Klimaanlage, grosszügige Sitzungszimmer-Suite, repräsentativer Empfangsbereich und Concierge-Service. Ideal für Finanzdienstleister, Family Offices, Vermögensverwalter. Mietvertrag min. 10 Jahre, ÖV-Anbindung 2 Min. (Tram, S-Bahn).', + units: [ + { id: 'unit-043-1', propertyId: 'prop-043', floorLevel: 6, unitLabel: '6.OG', areaSqm: 515, available: true, rentPricePerSqm: 840 }, + ], + importedFrom: 'SAP RE-FX', + importedAt: '2025-01-15T08:00:00Z', + lastUpdatedAt: '2025-05-20T10:00:00Z', + breakoutOption: false, + schattenmarktRelease: { enabled: false }, + organizationId: 'org-wincasa', + createdAt: '2025-05-22T10:00:00Z', + updatedAt: '2025-05-22T10:00:00Z', + }, + + { + id: 'prop-044', + title: 'Bürofläche Seefeldstrasse 18', + assetType: AssetType.OFFICE, + resultType: ResultType.VERIFIED_PORTFOLIO, + location: { city: 'Zürich', district: 'Seefeld', canton: 'ZH', country: 'CH', coordinates: { lat: 47.3592, lng: 8.5538 } }, + address: { street: 'Seefeldstrasse', houseNumber: '18', postalCode: '8008', city: 'Zürich', country: 'CH' }, + areaSqm: 495, + rentPricePerSqm: 810, + totalRentMonthly: 400950, + availabilityDate: '2026-07-01', + availabilityStatus: AvailabilityStatus.AVAILABLE_SOON, + sourceType: 'ERP_IMPORT', + confidenceScore: 0.97, + dataQuality: { + score: 0.95, + missingCriticalFields: [], + missingOptionalFields: [], + lastVerifiedAt: '2025-05-20', + freshness: DataFreshness.FRESH, + warnings: [], + }, + softFactors: { + prestige: 94, + accessibility: 92, + visibilityScore: 82, + talentAccess: 90, + parkingSpots: 8, + publicTransportMinutes: 3, + }, + hardFacts: { floor: 5, parking: 8, hasAirConditioning: true, fitOut: 'PREMIUM', ceilingHeightM: 3.1, isBarrierFree: true }, + floorLevel: 5, + contractDurationMonths: 120, + ancillaryCosts: 8.5, + riskLevel: RiskLevel.LOW, + images: ['https://images.unsplash.com/photo-1486325212027-8081e485255e?w=800&h=400&fit=crop'], + mapImageUrl: 'https://images.unsplash.com/photo-1524661135-423995f22d0b?w=600&h=300&fit=crop', + propertyNumber: 'ZH-2022-044', + description: 'Repräsentative Bürofläche im Seefeldquartier, nahe Bellevue und Zürichsee. 495 m² im 5. OG — helle Räume mit Seesicht, grosszügige Fensterflächen, Tageslicht von drei Seiten. PREMIUM-Ausbau mit Klimaanlage, 5 Sitzungszimmer, Empfangszone und Teeküche. 8 Tiefgaragenplätze. ÖV 3 Min. (Tram 2, 4). Mietvertrag mind. 10 Jahre — ideal für Wealth Management und professionelle Dienstleister.', + units: [ + { id: 'unit-044-1', propertyId: 'prop-044', floorLevel: 5, unitLabel: '5.OG', areaSqm: 495, available: true, rentPricePerSqm: 810 }, + ], + importedFrom: 'SAP RE-FX', + importedAt: '2025-01-15T08:00:00Z', + lastUpdatedAt: '2025-05-20T10:00:00Z', + breakoutOption: false, + schattenmarktRelease: { enabled: false }, + organizationId: 'org-wincasa', + createdAt: '2025-05-22T10:00:00Z', + updatedAt: '2025-05-22T10:00:00Z', + }, + + { + id: 'prop-045', + title: 'Bürofläche Limmatstrasse 268', + assetType: AssetType.OFFICE, + resultType: ResultType.VERIFIED_PORTFOLIO, + location: { city: 'Zürich', district: 'Zürich-West', canton: 'ZH', country: 'CH', coordinates: { lat: 47.3862, lng: 8.5105 } }, + address: { street: 'Limmatstrasse', houseNumber: '268', postalCode: '8005', city: 'Zürich', country: 'CH' }, + areaSqm: 175, + rentPricePerSqm: 295, + totalRentMonthly: 51625, + availabilityDate: '2025-10-01', + availabilityStatus: AvailabilityStatus.AVAILABLE_SOON, + sourceType: 'ERP_IMPORT', + confidenceScore: 0.95, + dataQuality: { + score: 0.91, + missingCriticalFields: [], + missingOptionalFields: ['expansionPotentialSqm'], + lastVerifiedAt: '2025-05-10', + freshness: DataFreshness.FRESH, + warnings: [], + }, + softFactors: { + prestige: 52, + accessibility: 82, + visibilityScore: 48, + talentAccess: 74, + parkingSpots: 4, + publicTransportMinutes: 7, + }, + hardFacts: { floor: 1, parking: 4, fitOut: 'BASIC', ceilingHeightM: 2.7 }, + floorLevel: 1, + expansionPotentialSqm: 80, + contractDurationMonths: 84, + ancillaryCosts: 4.0, + riskLevel: RiskLevel.LOW, + images: ['https://images.unsplash.com/photo-1497366811353-6870744d04b2?w=800&h=400&fit=crop'], + mapImageUrl: 'https://images.unsplash.com/photo-1524661135-423995f22d0b?w=600&h=300&fit=crop', + propertyNumber: 'ZH-2023-045', + description: 'Kompakte Bürofläche (175 m²) im Industrie-Groove von Zürich-West, Limmatstrasse. Platz für 10 Arbeitsplätze und 2 Besprechungsräume. Einfacher Ausbau mit hohem Tageslichtanteil, lebhaftes Startup-Umfeld mit Gemeinschaftskantine und Co-Working-Angebot im selben Gebäude. 4 Parkplätze im Innenhof. ÖV-Anbindung 7 Minuten zu Fuss. Mietvertrag bevorzugt 7 Jahre.', + units: [ + { id: 'unit-045-1', propertyId: 'prop-045', floorLevel: 1, unitLabel: '1.OG Süd', areaSqm: 175, available: true, rentPricePerSqm: 295 }, + ], + importedFrom: 'SAP RE-FX', + importedAt: '2025-01-15T08:00:00Z', + lastUpdatedAt: '2025-05-10T10:00:00Z', + breakoutOption: false, + schattenmarktRelease: { enabled: false }, + organizationId: 'org-wincasa', + createdAt: '2025-05-22T10:00:00Z', + updatedAt: '2025-05-22T10:00:00Z', + }, + // ───────────────────────────────────────────────────────────────────────────── // EXTERNAL_MARKET (10) // ───────────────────────────────────────────────────────────────────────────── @@ -1439,4 +1934,2556 @@ export const mockProperties: Property[] = [ updatedAt: '2026-04-15T10:30:00Z', }, + + // ── UC-A extras: Retail Zürich Kreise / Seegemeinden ───────────────────────── + + { + id: 'prop-050', + title: 'Ladenlokal Militärstrasse 80', + assetType: AssetType.RETAIL, + resultType: ResultType.VERIFIED_PORTFOLIO, + location: { city: 'Zürich', district: 'Kreis 5', canton: 'ZH', country: 'CH', coordinates: { lat: 47.382, lng: 8.521 } }, + address: { street: 'Militärstrasse', houseNumber: '80', postalCode: '8005', city: 'Zürich', country: 'CH' }, + areaSqm: 140, + rentPricePerSqm: 558, + totalRentMonthly: 78120, + availabilityDate: '2026-10-01', + availabilityStatus: AvailabilityStatus.AVAILABLE_SOON, + sourceType: 'ERP_IMPORT', + confidenceScore: 0.95, + dataQuality: { + score: 0.92, + missingCriticalFields: [], + missingOptionalFields: [], + lastVerifiedAt: '2025-05-15', + freshness: DataFreshness.FRESH, + warnings: [], + }, + softFactors: { + prestige: 65, + accessibility: 88, + visibilityScore: 82, + passerbyFrequency: 'HIGH', + talentAccess: 72, + parkingSpots: 0, + publicTransportMinutes: 4, + }, + hardFacts: { floor: 0, parking: 0, hasStorefront: true, fitOut: 'FULL', isBarrierFree: true }, + floorLevel: 0, + contractDurationMonths: 60, + ancillaryCosts: 6.5, + riskLevel: RiskLevel.LOW, + images: ['https://images.unsplash.com/photo-1441986300917-64674bd600d8?w=800&h=400&fit=crop'], + mapImageUrl: 'https://images.unsplash.com/photo-1524661135-423995f22d0b?w=600&h=300&fit=crop', + propertyNumber: 'ZH-2024-050', + description: 'Vollausgebautes Ladenlokal an der Militärstrasse, Zürich Kreis 5. Grosse Schaufensterfront, Erdgeschoss, hohe Passantenfrequenz. Verfügbar Oktober 2026.', + units: [ + { id: 'unit-050-1', propertyId: 'prop-050', floorLevel: 0, unitLabel: 'EG Laden', areaSqm: 140, available: true, rentPricePerSqm: 558 }, + ], + importedFrom: 'SAP RE-FX', + importedAt: '2025-01-15T08:00:00Z', + lastUpdatedAt: '2025-05-15T10:00:00Z', + breakoutOption: false, + schattenmarktRelease: { enabled: false }, + organizationId: 'org-wincasa', + createdAt: '2025-05-22T10:00:00Z', + updatedAt: '2025-05-22T10:00:00Z', + }, + + { + id: 'prop-051', + title: 'Ladenlokal Gotthardstrasse 12', + assetType: AssetType.RETAIL, + resultType: ResultType.VERIFIED_PORTFOLIO, + location: { city: 'Thalwil', district: 'Zentrum', canton: 'ZH', country: 'CH', coordinates: { lat: 47.292, lng: 8.566 } }, + address: { street: 'Gotthardstrasse', houseNumber: '12', postalCode: '8800', city: 'Thalwil', country: 'CH' }, + areaSqm: 132, + rentPricePerSqm: 338, + totalRentMonthly: 44616, + availabilityDate: '2026-11-01', + availabilityStatus: AvailabilityStatus.AVAILABLE_SOON, + sourceType: 'ERP_IMPORT', + confidenceScore: 0.92, + dataQuality: { + score: 0.89, + missingCriticalFields: [], + missingOptionalFields: [], + lastVerifiedAt: '2025-05-15', + freshness: DataFreshness.FRESH, + warnings: [], + }, + softFactors: { + prestige: 48, + accessibility: 78, + visibilityScore: 68, + passerbyFrequency: 'MEDIUM_HIGH', + talentAccess: 60, + parkingSpots: 2, + publicTransportMinutes: 5, + }, + hardFacts: { floor: 0, parking: 2, hasStorefront: true, fitOut: 'FULL', isBarrierFree: true }, + floorLevel: 0, + contractDurationMonths: 60, + ancillaryCosts: 5.5, + riskLevel: RiskLevel.LOW, + images: ['https://images.unsplash.com/photo-1555529669-e69e7aa0ba9a?w=800&h=400&fit=crop'], + mapImageUrl: 'https://images.unsplash.com/photo-1524661135-423995f22d0b?w=600&h=300&fit=crop', + propertyNumber: 'ZH-2024-051', + description: 'Ladenlokal an der Gotthardstrasse, Thalwil — Seegemeinde mit guter S-Bahn-Anbindung. EG mit Schaufensterfront, vollständig ausgebaut. Verfügbar November 2026.', + units: [ + { id: 'unit-051-1', propertyId: 'prop-051', floorLevel: 0, unitLabel: 'EG Laden', areaSqm: 132, available: true, rentPricePerSqm: 338 }, + ], + importedFrom: 'SAP RE-FX', + importedAt: '2025-01-15T08:00:00Z', + lastUpdatedAt: '2025-05-15T10:00:00Z', + breakoutOption: false, + schattenmarktRelease: { enabled: false }, + organizationId: 'org-wincasa', + createdAt: '2025-05-22T10:00:00Z', + updatedAt: '2025-05-22T10:00:00Z', + }, + + { + id: 'prop-052', + title: 'Ladenlokal Bahnhofstrasse 22, Uster', + assetType: AssetType.RETAIL, + resultType: ResultType.VERIFIED_PORTFOLIO, + location: { city: 'Uster', district: 'Zentrum', canton: 'ZH', country: 'CH', coordinates: { lat: 47.352, lng: 8.720 } }, + address: { street: 'Bahnhofstrasse', houseNumber: '22', postalCode: '8610', city: 'Uster', country: 'CH' }, + areaSqm: 128, + rentPricePerSqm: 312, + totalRentMonthly: 39936, + availabilityDate: '2026-10-01', + availabilityStatus: AvailabilityStatus.AVAILABLE_SOON, + sourceType: 'ERP_IMPORT', + confidenceScore: 0.91, + dataQuality: { + score: 0.88, + missingCriticalFields: [], + missingOptionalFields: [], + lastVerifiedAt: '2025-05-15', + freshness: DataFreshness.FRESH, + warnings: [], + }, + softFactors: { + prestige: 44, + accessibility: 82, + visibilityScore: 62, + passerbyFrequency: 'MEDIUM', + talentAccess: 55, + parkingSpots: 3, + publicTransportMinutes: 4, + }, + hardFacts: { floor: 0, parking: 3, hasStorefront: true, fitOut: 'FULL', isBarrierFree: true }, + floorLevel: 0, + contractDurationMonths: 60, + ancillaryCosts: 5.0, + riskLevel: RiskLevel.LOW, + images: ['https://images.unsplash.com/photo-1528698827591-e19ccd7bc23d?w=800&h=400&fit=crop'], + mapImageUrl: 'https://images.unsplash.com/photo-1524661135-423995f22d0b?w=600&h=300&fit=crop', + propertyNumber: 'ZH-2024-052', + description: 'Vollausgebautes Ladenlokal an der Bahnhofstrasse Uster. EG, Schaufensterfront, gute Passantenfrequenz nahe Bahnhof. Verfügbar Oktober 2026.', + units: [ + { id: 'unit-052-1', propertyId: 'prop-052', floorLevel: 0, unitLabel: 'EG Laden', areaSqm: 128, available: true, rentPricePerSqm: 312 }, + ], + importedFrom: 'SAP RE-FX', + importedAt: '2025-01-15T08:00:00Z', + lastUpdatedAt: '2025-05-15T10:00:00Z', + breakoutOption: false, + schattenmarktRelease: { enabled: false }, + organizationId: 'org-wincasa', + createdAt: '2025-05-22T10:00:00Z', + updatedAt: '2025-05-22T10:00:00Z', + }, + + { + id: 'prop-053', + title: 'Ladenlokal Langstrasse 18', + assetType: AssetType.RETAIL, + resultType: ResultType.VERIFIED_PORTFOLIO, + location: { city: 'Zürich', district: 'Kreis 4', canton: 'ZH', country: 'CH', coordinates: { lat: 47.375, lng: 8.527 } }, + address: { street: 'Langstrasse', houseNumber: '18', postalCode: '8004', city: 'Zürich', country: 'CH' }, + areaSqm: 145, + rentPricePerSqm: 542, + totalRentMonthly: 78590, + availabilityDate: '2027-01-01', + availabilityStatus: AvailabilityStatus.AVAILABLE_SOON, + sourceType: 'ERP_IMPORT', + confidenceScore: 0.94, + dataQuality: { + score: 0.91, + missingCriticalFields: [], + missingOptionalFields: [], + lastVerifiedAt: '2025-05-15', + freshness: DataFreshness.FRESH, + warnings: [], + }, + softFactors: { + prestige: 64, + accessibility: 90, + visibilityScore: 80, + passerbyFrequency: 'HIGH', + talentAccess: 70, + parkingSpots: 0, + publicTransportMinutes: 3, + }, + hardFacts: { floor: 0, parking: 0, hasStorefront: true, fitOut: 'FULL', isBarrierFree: true }, + floorLevel: 0, + contractDurationMonths: 60, + ancillaryCosts: 6.5, + riskLevel: RiskLevel.LOW, + images: ['https://images.unsplash.com/photo-1441986300917-64674bd600d8?w=800&h=400&fit=crop'], + mapImageUrl: 'https://images.unsplash.com/photo-1524661135-423995f22d0b?w=600&h=300&fit=crop', + propertyNumber: 'ZH-2024-053', + description: 'Ladenlokal an der Langstrasse, Zürich Kreis 4. Belebter Standort mit hoher Fussgängerfrequenz, grosser Schaufensterfront. Vollständig ausgebaut, EG. Verfügbar Januar 2027.', + units: [ + { id: 'unit-053-1', propertyId: 'prop-053', floorLevel: 0, unitLabel: 'EG Laden', areaSqm: 145, available: true, rentPricePerSqm: 542 }, + ], + importedFrom: 'SAP RE-FX', + importedAt: '2025-01-15T08:00:00Z', + lastUpdatedAt: '2025-05-15T10:00:00Z', + breakoutOption: false, + schattenmarktRelease: { enabled: false }, + organizationId: 'org-wincasa', + createdAt: '2025-05-22T10:00:00Z', + updatedAt: '2025-05-22T10:00:00Z', + }, + + { + id: 'prop-054', + title: 'Ladenlokal Kreuzstrasse 24', + assetType: AssetType.RETAIL, + resultType: ResultType.VERIFIED_PORTFOLIO, + location: { city: 'Zürich', district: 'Kreis 8', canton: 'ZH', country: 'CH', coordinates: { lat: 47.356, lng: 8.550 } }, + address: { street: 'Kreuzstrasse', houseNumber: '24', postalCode: '8008', city: 'Zürich', country: 'CH' }, + areaSqm: 138, + rentPricePerSqm: 575, + totalRentMonthly: 79350, + availabilityDate: '2026-11-01', + availabilityStatus: AvailabilityStatus.AVAILABLE_SOON, + sourceType: 'ERP_IMPORT', + confidenceScore: 0.93, + dataQuality: { + score: 0.90, + missingCriticalFields: [], + missingOptionalFields: [], + lastVerifiedAt: '2025-05-15', + freshness: DataFreshness.FRESH, + warnings: [], + }, + softFactors: { + prestige: 70, + accessibility: 88, + visibilityScore: 76, + passerbyFrequency: 'HIGH', + talentAccess: 74, + parkingSpots: 0, + publicTransportMinutes: 4, + }, + hardFacts: { floor: 0, parking: 0, hasStorefront: true, fitOut: 'FULL', isBarrierFree: true }, + floorLevel: 0, + contractDurationMonths: 60, + ancillaryCosts: 7.0, + riskLevel: RiskLevel.LOW, + images: ['https://images.unsplash.com/photo-1555529669-e69e7aa0ba9a?w=800&h=400&fit=crop'], + mapImageUrl: 'https://images.unsplash.com/photo-1524661135-423995f22d0b?w=600&h=300&fit=crop', + propertyNumber: 'ZH-2024-054', + description: 'Exklusives Ladenlokal im Seefeld-Quartier, Zürich Kreis 8. EG, Schaufensterfront, gehobene Lage. Idealer Standort für Lifestyle- und Premiummarken. Verfügbar November 2026.', + units: [ + { id: 'unit-054-1', propertyId: 'prop-054', floorLevel: 0, unitLabel: 'EG Laden', areaSqm: 138, available: true, rentPricePerSqm: 575 }, + ], + importedFrom: 'SAP RE-FX', + importedAt: '2025-01-15T08:00:00Z', + lastUpdatedAt: '2025-05-15T10:00:00Z', + breakoutOption: false, + schattenmarktRelease: { enabled: false }, + organizationId: 'org-wincasa', + createdAt: '2025-05-22T10:00:00Z', + updatedAt: '2025-05-22T10:00:00Z', + }, + + { + id: 'prop-055', + title: 'Ladenfläche Weststrasse 44', + assetType: AssetType.RETAIL, + resultType: ResultType.EXTERNAL_MARKET, + location: { city: 'Zürich', district: 'Kreis 3', canton: 'ZH', country: 'CH', coordinates: { lat: 47.372, lng: 8.521 } }, + address: { street: 'Weststrasse', houseNumber: '44', postalCode: '8003', city: 'Zürich', country: 'CH' }, + areaSqm: 150, + rentPricePerSqm: 505, + totalRentMonthly: 75750, + availabilityDate: '2026-06-01', + availabilityStatus: AvailabilityStatus.AVAILABLE_NOW, + sourceType: 'IMMOSCOUT_SCRAPE', + sourceUrl: 'https://example.com/listing/prop-055', + confidenceScore: 0.70, + dataQuality: { + score: 0.65, + missingCriticalFields: ['contractDurationMonths'], + missingOptionalFields: ['ancillaryCosts'], + lastVerifiedAt: '2025-05-10', + freshness: DataFreshness.STALE, + warnings: ['Daten aus Drittquelle'], + }, + softFactors: { + prestige: 58, + accessibility: 85, + visibilityScore: 70, + passerbyFrequency: 'MEDIUM_HIGH', + talentAccess: 65, + parkingSpots: 2, + publicTransportMinutes: 5, + }, + hardFacts: { floor: 0, parking: 2, hasStorefront: true, fitOut: 'FULL', isBarrierFree: true }, + floorLevel: 0, + contractDurationMonths: 48, + ancillaryCosts: 6.0, + riskLevel: RiskLevel.MEDIUM, + images: ['https://images.unsplash.com/photo-1534398079543-7ae6d016b86a?w=800&h=400&fit=crop'], + mapImageUrl: 'https://images.unsplash.com/photo-1524661135-423995f22d0b?w=600&h=300&fit=crop', + propertyNumber: 'ZH-EXT-055', + description: 'Ladenfläche an der Weststrasse, Zürich Kreis 3. EG mit Schaufensterfront, gut ausgebaut. Mittlere bis hohe Passantenfrequenz, Tramanbindung direkt.', + units: [ + { id: 'unit-055-1', propertyId: 'prop-055', floorLevel: 0, areaSqm: 150, available: true, rentPricePerSqm: 505 }, + ], + createdAt: '2025-05-10T08:00:00Z', + updatedAt: '2025-05-10T08:00:00Z', + }, + + { + id: 'prop-056', + title: 'Gewerbefläche Hohlstrasse 88', + assetType: AssetType.RETAIL, + resultType: ResultType.EXTERNAL_MARKET, + location: { city: 'Zürich', district: 'Kreis 4', canton: 'ZH', country: 'CH', coordinates: { lat: 47.375, lng: 8.527 } }, + address: { street: 'Hohlstrasse', houseNumber: '88', postalCode: '8004', city: 'Zürich', country: 'CH' }, + areaSqm: 155, + rentPricePerSqm: 555, + totalRentMonthly: 86025, + availabilityDate: '2026-06-01', + availabilityStatus: AvailabilityStatus.AVAILABLE_NOW, + sourceType: 'HOMEGATE_SCRAPE', + sourceUrl: 'https://example.com/listing/prop-056', + confidenceScore: 0.68, + dataQuality: { + score: 0.62, + missingCriticalFields: ['contractDurationMonths'], + missingOptionalFields: ['ancillaryCosts'], + lastVerifiedAt: '2025-05-08', + freshness: DataFreshness.STALE, + warnings: ['Daten aus Drittquelle', 'Schaufensterfront nicht bestätigt', 'OG-Lage — kein EG'], + }, + softFactors: { + prestige: 55, + accessibility: 82, + visibilityScore: 48, + passerbyFrequency: 'MEDIUM', + talentAccess: 62, + parkingSpots: 0, + publicTransportMinutes: 5, + }, + hardFacts: { floor: 1, parking: 0, hasStorefront: false, fitOut: 'BASIC', isBarrierFree: false }, + floorLevel: 1, + contractDurationMonths: 48, + ancillaryCosts: 5.5, + riskLevel: RiskLevel.MEDIUM, + images: ['https://images.unsplash.com/photo-1497366216548-37526070297c?w=800&h=400&fit=crop'], + mapImageUrl: 'https://images.unsplash.com/photo-1524661135-423995f22d0b?w=600&h=300&fit=crop', + propertyNumber: 'ZH-EXT-056', + description: 'Gewerbefläche an der Hohlstrasse, Zürich Kreis 4. 1. OG, kein Schaufenster — Near-Miss für Retail-Anfragen mit EG-Anforderung. Basisausbau.', + units: [ + { id: 'unit-056-1', propertyId: 'prop-056', floorLevel: 1, areaSqm: 155, available: true, rentPricePerSqm: 555 }, + ], + createdAt: '2025-05-08T08:00:00Z', + updatedAt: '2025-05-08T08:00:00Z', + }, + + { + id: 'prop-057', + title: 'Ladenlokal Winterthurerstrasse 28', + assetType: AssetType.RETAIL, + resultType: ResultType.MAISON_WORK, + location: { city: 'Zürich', district: 'Kreis 6', canton: 'ZH', country: 'CH', coordinates: { lat: 47.384, lng: 8.542 } }, + address: { street: 'Winterthurerstrasse', houseNumber: '28', postalCode: '8006', city: 'Zürich', country: 'CH' }, + areaSqm: 142, + rentPricePerSqm: 528, + totalRentMonthly: 74976, + availabilityDate: '2026-06-01', + availabilityStatus: AvailabilityStatus.AVAILABLE_NOW, + sourceType: 'MAISON_WORK_SCRAPE', + sourceUrl: 'https://example.com/listing/prop-057', + confidenceScore: 0.75, + dataQuality: { + score: 0.70, + missingCriticalFields: [], + missingOptionalFields: ['ancillaryCosts', 'expansionPotentialSqm'], + lastVerifiedAt: '2025-05-12', + freshness: DataFreshness.STALE, + warnings: ['Ausbaustandard nicht final bestätigt'], + }, + softFactors: { + prestige: 58, + accessibility: 84, + visibilityScore: 65, + passerbyFrequency: 'MEDIUM', + talentAccess: 68, + parkingSpots: 2, + publicTransportMinutes: 5, + }, + hardFacts: { floor: 0, parking: 2, hasStorefront: true, fitOut: 'FULL', isBarrierFree: true }, + floorLevel: 0, + contractDurationMonths: 48, + ancillaryCosts: 6.0, + riskLevel: RiskLevel.MEDIUM, + images: ['https://images.unsplash.com/photo-1528698827591-e19ccd7bc23d?w=800&h=400&fit=crop'], + mapImageUrl: 'https://images.unsplash.com/photo-1524661135-423995f22d0b?w=600&h=300&fit=crop', + propertyNumber: 'ZH-MW-057', + description: 'Ladenlokal an der Winterthurerstrasse, Zürich Kreis 6. EG, Schaufensterfront, mittlere Passantenfrequenz. Vollausbau gemäss Inserat.', + units: [ + { id: 'unit-057-1', propertyId: 'prop-057', floorLevel: 0, areaSqm: 142, available: true, rentPricePerSqm: 528 }, + ], + createdAt: '2025-05-12T08:00:00Z', + updatedAt: '2025-05-12T08:00:00Z', + }, + + { + id: 'prop-058', + title: 'Ladenlokal Grubenstrasse 18', + assetType: AssetType.RETAIL, + resultType: ResultType.VERIFIED_PORTFOLIO, + location: { city: 'Zürich', district: 'Kreis 3', canton: 'ZH', country: 'CH', coordinates: { lat: 47.372, lng: 8.521 } }, + address: { street: 'Grubenstrasse', houseNumber: '18', postalCode: '8003', city: 'Zürich', country: 'CH' }, + areaSqm: 125, + rentPricePerSqm: 492, + totalRentMonthly: 61500, + availabilityDate: '2026-10-01', + availabilityStatus: AvailabilityStatus.AVAILABLE_SOON, + sourceType: 'ERP_IMPORT', + confidenceScore: 0.92, + dataQuality: { + score: 0.89, + missingCriticalFields: [], + missingOptionalFields: [], + lastVerifiedAt: '2025-05-15', + freshness: DataFreshness.FRESH, + warnings: [], + }, + softFactors: { + prestige: 55, + accessibility: 82, + visibilityScore: 70, + passerbyFrequency: 'MEDIUM_HIGH', + talentAccess: 65, + parkingSpots: 0, + publicTransportMinutes: 5, + }, + hardFacts: { floor: 0, parking: 0, hasStorefront: true, fitOut: 'FULL', isBarrierFree: true }, + floorLevel: 0, + contractDurationMonths: 60, + ancillaryCosts: 6.0, + riskLevel: RiskLevel.LOW, + images: ['https://images.unsplash.com/photo-1441986300917-64674bd600d8?w=800&h=400&fit=crop'], + mapImageUrl: 'https://images.unsplash.com/photo-1524661135-423995f22d0b?w=600&h=300&fit=crop', + propertyNumber: 'ZH-2024-058', + description: 'Ladenlokal an der Grubenstrasse, Zürich Kreis 3. EG, Schaufensterfront, mittlere bis hohe Passantenfrequenz. Vollständig ausgebaut. Verfügbar Oktober 2026.', + units: [ + { id: 'unit-058-1', propertyId: 'prop-058', floorLevel: 0, unitLabel: 'EG Laden', areaSqm: 125, available: true, rentPricePerSqm: 492 }, + ], + importedFrom: 'SAP RE-FX', + importedAt: '2025-01-15T08:00:00Z', + lastUpdatedAt: '2025-05-15T10:00:00Z', + breakoutOption: false, + schattenmarktRelease: { enabled: false }, + organizationId: 'org-wincasa', + createdAt: '2025-05-22T10:00:00Z', + updatedAt: '2025-05-22T10:00:00Z', + }, + + { + id: 'prop-059', + title: 'Ladenlokal Quellenstrasse 25 (Signal: Auszug)', + assetType: AssetType.RETAIL, + resultType: ResultType.FUTURE_AVAILABILITY, + location: { city: 'Zürich', district: 'Kreis 5', canton: 'ZH', country: 'CH', coordinates: { lat: 47.382, lng: 8.521 } }, + address: { street: 'Quellenstrasse', houseNumber: '25', postalCode: '8005', city: 'Zürich', country: 'CH' }, + areaSqm: 135, + rentPricePerSqm: 565, + availabilityDate: '2026-11-01', + availabilityStatus: AvailabilityStatus.AVAILABLE_SOON, + sourceType: 'ERP_IMPORT', + confidenceScore: 0.82, + dataQuality: { + score: 0.78, + missingCriticalFields: [], + missingOptionalFields: ['expansionPotentialSqm'], + lastVerifiedAt: '2025-05-15', + freshness: DataFreshness.FRESH, + warnings: ['Aktuell vermietet — Pre-Market-Freigabe aktiv'], + }, + softFactors: { + prestige: 63, + accessibility: 88, + visibilityScore: 74, + passerbyFrequency: 'HIGH', + talentAccess: 70, + parkingSpots: 0, + publicTransportMinutes: 4, + }, + hardFacts: { floor: 0, parking: 0, hasStorefront: true, fitOut: 'FULL', isBarrierFree: true }, + floorLevel: 0, + contractDurationMonths: 60, + ancillaryCosts: 6.5, + riskLevel: RiskLevel.LOW, + images: ['https://images.unsplash.com/photo-1441986300917-64674bd600d8?w=800&h=400&fit=crop'], + mapImageUrl: 'https://images.unsplash.com/photo-1524661135-423995f22d0b?w=600&h=300&fit=crop', + propertyNumber: 'ZH-2024-059', + description: 'Ladenlokal an der Quellenstrasse, Zürich Kreis 5. Derzeit von Textilhandel GmbH belegt — Auszug per November 2026 bekannt. Pre-Market-Freigabe aktiv. EG, Schaufensterfront, hohe Passantenfrequenz.', + units: [ + { id: 'unit-059-1', propertyId: 'prop-059', floorLevel: 0, unitLabel: 'EG Laden', areaSqm: 135, available: false, rentPricePerSqm: 565, currentTenant: 'Textilhandel GmbH', leaseEndDate: '2026-11-01', schattenmarktRelease: { enabled: true, availableFrom: '2026-11-01' } }, + ], + importedFrom: 'SAP RE-FX', + importedAt: '2025-01-15T08:00:00Z', + lastUpdatedAt: '2025-05-15T10:00:00Z', + currentTenant: 'Textilhandel GmbH', + leaseEndDate: '2026-11-01', + breakoutOption: false, + schattenmarktRelease: { enabled: true, leadTimeMonths: 6 }, + organizationId: 'org-wincasa', + createdAt: '2025-05-22T10:00:00Z', + updatedAt: '2025-05-22T10:00:00Z', + }, + + // ── UC-B extras: Office Zürich-West / Kreis 5 / Kreis 4, BASIC ─────────────── + + { + id: 'prop-060', + title: 'Bürofläche Hardturmstrasse 22', + assetType: AssetType.OFFICE, + resultType: ResultType.VERIFIED_PORTFOLIO, + location: { city: 'Zürich', district: 'Zürich-West', canton: 'ZH', country: 'CH', coordinates: { lat: 47.388, lng: 8.510 } }, + address: { street: 'Hardturmstrasse', houseNumber: '22', postalCode: '8005', city: 'Zürich', country: 'CH' }, + areaSqm: 180, + rentPricePerSqm: 275, + totalRentMonthly: 49500, + availabilityDate: '2025-10-01', + availabilityStatus: AvailabilityStatus.AVAILABLE_SOON, + sourceType: 'ERP_IMPORT', + confidenceScore: 0.95, + dataQuality: { + score: 0.92, + missingCriticalFields: [], + missingOptionalFields: [], + lastVerifiedAt: '2025-05-15', + freshness: DataFreshness.FRESH, + warnings: [], + }, + softFactors: { + prestige: 58, + accessibility: 86, + visibilityScore: 50, + talentAccess: 80, + parkingSpots: 5, + publicTransportMinutes: 6, + }, + hardFacts: { floor: 2, parking: 5, fitOut: 'BASIC', ceilingHeightM: 2.8 }, + floorLevel: 2, + contractDurationMonths: 84, + ancillaryCosts: 3.8, + riskLevel: RiskLevel.LOW, + images: ['https://images.unsplash.com/photo-1497366216548-37526070297c?w=800&h=400&fit=crop'], + mapImageUrl: 'https://images.unsplash.com/photo-1524661135-423995f22d0b?w=600&h=300&fit=crop', + propertyNumber: 'ZH-2023-060', + description: 'Bürofläche im 2. OG an der Hardturmstrasse, Zürich-West. 180 m², Basisausbau, 5 Parkplätze im Hof. Dynamisches Industrie-Umfeld, gute ÖV-Anbindung. Verfügbar Oktober 2025.', + units: [ + { id: 'unit-060-1', propertyId: 'prop-060', floorLevel: 2, unitLabel: '2.OG', areaSqm: 180, available: true, rentPricePerSqm: 275 }, + ], + importedFrom: 'SAP RE-FX', + importedAt: '2025-01-15T08:00:00Z', + lastUpdatedAt: '2025-05-15T10:00:00Z', + breakoutOption: false, + schattenmarktRelease: { enabled: false }, + organizationId: 'org-wincasa', + createdAt: '2025-05-22T10:00:00Z', + updatedAt: '2025-05-22T10:00:00Z', + }, + + { + id: 'prop-061', + title: 'Bürofläche Herdernstrasse 64', + assetType: AssetType.OFFICE, + resultType: ResultType.VERIFIED_PORTFOLIO, + location: { city: 'Zürich', district: 'Kreis 5', canton: 'ZH', country: 'CH', coordinates: { lat: 47.382, lng: 8.521 } }, + address: { street: 'Herdernstrasse', houseNumber: '64', postalCode: '8005', city: 'Zürich', country: 'CH' }, + areaSqm: 165, + rentPricePerSqm: 262, + totalRentMonthly: 43230, + availabilityDate: '2026-01-01', + availabilityStatus: AvailabilityStatus.AVAILABLE_SOON, + sourceType: 'ERP_IMPORT', + confidenceScore: 0.93, + dataQuality: { + score: 0.90, + missingCriticalFields: [], + missingOptionalFields: [], + lastVerifiedAt: '2025-05-15', + freshness: DataFreshness.FRESH, + warnings: [], + }, + softFactors: { + prestige: 56, + accessibility: 88, + visibilityScore: 48, + talentAccess: 78, + parkingSpots: 3, + publicTransportMinutes: 5, + }, + hardFacts: { floor: 1, parking: 3, fitOut: 'BASIC', ceilingHeightM: 2.8 }, + floorLevel: 1, + contractDurationMonths: 84, + ancillaryCosts: 3.5, + riskLevel: RiskLevel.LOW, + images: ['https://images.unsplash.com/photo-1522071820081-009f0129c71c?w=800&h=400&fit=crop'], + mapImageUrl: 'https://images.unsplash.com/photo-1524661135-423995f22d0b?w=600&h=300&fit=crop', + propertyNumber: 'ZH-2023-061', + description: 'Bürofläche im 1. OG, Herdernstrasse Kreis 5. 165 m², Basisausbau, 3 Parkplätze. Lebhaftes Umfeld, kurze Wege zu ÖV. Verfügbar Januar 2026.', + units: [ + { id: 'unit-061-1', propertyId: 'prop-061', floorLevel: 1, unitLabel: '1.OG', areaSqm: 165, available: true, rentPricePerSqm: 262 }, + ], + importedFrom: 'SAP RE-FX', + importedAt: '2025-01-15T08:00:00Z', + lastUpdatedAt: '2025-05-15T10:00:00Z', + breakoutOption: false, + schattenmarktRelease: { enabled: false }, + organizationId: 'org-wincasa', + createdAt: '2025-05-22T10:00:00Z', + updatedAt: '2025-05-22T10:00:00Z', + }, + + { + id: 'prop-062', + title: 'Bürofläche Militärstrasse 36', + assetType: AssetType.OFFICE, + resultType: ResultType.VERIFIED_PORTFOLIO, + location: { city: 'Zürich', district: 'Kreis 4', canton: 'ZH', country: 'CH', coordinates: { lat: 47.375, lng: 8.527 } }, + address: { street: 'Militärstrasse', houseNumber: '36', postalCode: '8004', city: 'Zürich', country: 'CH' }, + areaSqm: 195, + rentPricePerSqm: 302, + totalRentMonthly: 58890, + availabilityDate: '2026-04-01', + availabilityStatus: AvailabilityStatus.AVAILABLE_SOON, + sourceType: 'ERP_IMPORT', + confidenceScore: 0.94, + dataQuality: { + score: 0.91, + missingCriticalFields: [], + missingOptionalFields: [], + lastVerifiedAt: '2025-05-15', + freshness: DataFreshness.FRESH, + warnings: [], + }, + softFactors: { + prestige: 60, + accessibility: 90, + visibilityScore: 52, + talentAccess: 72, + parkingSpots: 4, + publicTransportMinutes: 4, + }, + hardFacts: { floor: 3, parking: 4, fitOut: 'BASIC', ceilingHeightM: 2.8 }, + floorLevel: 3, + contractDurationMonths: 84, + ancillaryCosts: 4.0, + riskLevel: RiskLevel.LOW, + images: ['https://images.unsplash.com/photo-1524758631624-e2822e304c36?w=800&h=400&fit=crop'], + mapImageUrl: 'https://images.unsplash.com/photo-1524661135-423995f22d0b?w=600&h=300&fit=crop', + propertyNumber: 'ZH-2023-062', + description: 'Bürofläche im 3. OG, Militärstrasse 36, Zürich Kreis 4. 195 m², Basisausbau, 4 Parkplätze. Gute Erreichbarkeit per Tram und S-Bahn. Verfügbar April 2026.', + units: [ + { id: 'unit-062-1', propertyId: 'prop-062', floorLevel: 3, unitLabel: '3.OG', areaSqm: 195, available: true, rentPricePerSqm: 302 }, + ], + importedFrom: 'SAP RE-FX', + importedAt: '2025-01-15T08:00:00Z', + lastUpdatedAt: '2025-05-15T10:00:00Z', + breakoutOption: false, + schattenmarktRelease: { enabled: false }, + organizationId: 'org-wincasa', + createdAt: '2025-05-22T10:00:00Z', + updatedAt: '2025-05-22T10:00:00Z', + }, + + { + id: 'prop-063', + title: 'Büroloft Schiffbaustrasse 14', + assetType: AssetType.OFFICE, + resultType: ResultType.VERIFIED_PORTFOLIO, + location: { city: 'Zürich', district: 'Zürich-West', canton: 'ZH', country: 'CH', coordinates: { lat: 47.388, lng: 8.510 } }, + address: { street: 'Schiffbaustrasse', houseNumber: '14', postalCode: '8005', city: 'Zürich', country: 'CH' }, + areaSqm: 170, + rentPricePerSqm: 288, + totalRentMonthly: 48960, + availabilityDate: '2026-01-01', + availabilityStatus: AvailabilityStatus.AVAILABLE_SOON, + sourceType: 'ERP_IMPORT', + confidenceScore: 0.94, + dataQuality: { + score: 0.91, + missingCriticalFields: [], + missingOptionalFields: [], + lastVerifiedAt: '2025-05-15', + freshness: DataFreshness.FRESH, + warnings: [], + }, + softFactors: { + prestige: 62, + accessibility: 84, + visibilityScore: 55, + talentAccess: 82, + parkingSpots: 0, + publicTransportMinutes: 6, + }, + hardFacts: { floor: 1, parking: 0, fitOut: 'BASIC', ceilingHeightM: 3.4 }, + floorLevel: 1, + contractDurationMonths: 84, + ancillaryCosts: 4.0, + riskLevel: RiskLevel.LOW, + images: ['https://images.unsplash.com/photo-1497366216548-37526070297c?w=800&h=400&fit=crop'], + mapImageUrl: 'https://images.unsplash.com/photo-1524661135-423995f22d0b?w=600&h=300&fit=crop', + propertyNumber: 'ZH-2023-063', + description: 'Büroloft im ehemaligen Schiffbau-Komplex, Zürich-West. 170 m², hohe Decken (3.4 m), Industriecharakter, Startup- und Community-Atmosphäre. Keine eigenen Parkplätze, hervorragende ÖV-Anbindung. Verfügbar Januar 2026.', + units: [ + { id: 'unit-063-1', propertyId: 'prop-063', floorLevel: 1, unitLabel: '1.OG Loft', areaSqm: 170, available: true, rentPricePerSqm: 288 }, + ], + importedFrom: 'SAP RE-FX', + importedAt: '2025-01-15T08:00:00Z', + lastUpdatedAt: '2025-05-15T10:00:00Z', + breakoutOption: false, + schattenmarktRelease: { enabled: false }, + organizationId: 'org-wincasa', + createdAt: '2025-05-22T10:00:00Z', + updatedAt: '2025-05-22T10:00:00Z', + }, + + { + id: 'prop-064', + title: 'Bürofläche Förrlibuckstrasse 22', + assetType: AssetType.OFFICE, + resultType: ResultType.EXTERNAL_MARKET, + location: { city: 'Zürich', district: 'Zürich-West', canton: 'ZH', country: 'CH', coordinates: { lat: 47.388, lng: 8.510 } }, + address: { street: 'Förrlibuckstrasse', houseNumber: '22', postalCode: '8005', city: 'Zürich', country: 'CH' }, + areaSqm: 185, + rentPricePerSqm: 308, + totalRentMonthly: 56980, + availabilityDate: '2026-06-01', + availabilityStatus: AvailabilityStatus.AVAILABLE_NOW, + sourceType: 'IMMOSCOUT_SCRAPE', + sourceUrl: 'https://example.com/listing/prop-064', + confidenceScore: 0.72, + dataQuality: { + score: 0.68, + missingCriticalFields: ['contractDurationMonths'], + missingOptionalFields: ['ancillaryCosts'], + lastVerifiedAt: '2025-05-10', + freshness: DataFreshness.STALE, + warnings: ['Daten aus Drittquelle'], + }, + softFactors: { + prestige: 60, + accessibility: 84, + visibilityScore: 50, + talentAccess: 78, + parkingSpots: 4, + publicTransportMinutes: 7, + }, + hardFacts: { floor: 2, parking: 4, fitOut: 'BASIC', ceilingHeightM: 2.8 }, + floorLevel: 2, + contractDurationMonths: 60, + ancillaryCosts: 4.0, + riskLevel: RiskLevel.MEDIUM, + images: ['https://images.unsplash.com/photo-1522071820081-009f0129c71c?w=800&h=400&fit=crop'], + mapImageUrl: 'https://images.unsplash.com/photo-1524661135-423995f22d0b?w=600&h=300&fit=crop', + propertyNumber: 'ZH-EXT-064', + description: 'Bürofläche im 2. OG, Förrlibuckstrasse, Zürich-West. 185 m², Basisausbau, 4 Parkplätze. Gutes Preis-Leistungs-Verhältnis für das Umfeld.', + units: [ + { id: 'unit-064-1', propertyId: 'prop-064', floorLevel: 2, areaSqm: 185, available: true, rentPricePerSqm: 308 }, + ], + createdAt: '2025-05-10T08:00:00Z', + updatedAt: '2025-05-10T08:00:00Z', + }, + + { + id: 'prop-065', + title: 'Bürofläche Binzmühlestrasse 95, Oerlikon', + assetType: AssetType.OFFICE, + resultType: ResultType.EXTERNAL_MARKET, + location: { city: 'Zürich', district: 'Oerlikon', canton: 'ZH', country: 'CH', coordinates: { lat: 47.411, lng: 8.544 } }, + address: { street: 'Binzmühlestrasse', houseNumber: '95', postalCode: '8050', city: 'Zürich', country: 'CH' }, + areaSqm: 175, + rentPricePerSqm: 252, + totalRentMonthly: 44100, + availabilityDate: '2026-06-01', + availabilityStatus: AvailabilityStatus.AVAILABLE_NOW, + sourceType: 'NEWHOME_SCRAPE', + sourceUrl: 'https://example.com/listing/prop-065', + confidenceScore: 0.70, + dataQuality: { + score: 0.65, + missingCriticalFields: ['contractDurationMonths'], + missingOptionalFields: ['ancillaryCosts'], + lastVerifiedAt: '2025-05-08', + freshness: DataFreshness.STALE, + warnings: ['Daten aus Drittquelle', 'Standort ausserhalb Zielperimeter UC-B'], + }, + softFactors: { + prestige: 52, + accessibility: 80, + visibilityScore: 44, + talentAccess: 68, + parkingSpots: 6, + publicTransportMinutes: 8, + }, + hardFacts: { floor: 4, parking: 6, fitOut: 'BASIC', ceilingHeightM: 2.7 }, + floorLevel: 4, + contractDurationMonths: 60, + ancillaryCosts: 3.5, + riskLevel: RiskLevel.MEDIUM, + images: ['https://images.unsplash.com/photo-1524758631624-e2822e304c36?w=800&h=400&fit=crop'], + mapImageUrl: 'https://images.unsplash.com/photo-1524661135-423995f22d0b?w=600&h=300&fit=crop', + propertyNumber: 'ZH-EXT-065', + description: 'Bürofläche im 4. OG, Binzmühlestrasse Oerlikon. 175 m², Basisausbau, günstiger Mietpreis — jedoch ausserhalb des Wunschperimeters Zürich-West/Kreis 4–5.', + units: [ + { id: 'unit-065-1', propertyId: 'prop-065', floorLevel: 4, areaSqm: 175, available: true, rentPricePerSqm: 252 }, + ], + createdAt: '2025-05-08T08:00:00Z', + updatedAt: '2025-05-08T08:00:00Z', + }, + + { + id: 'prop-066', + title: 'Bürofläche Technoparkstrasse 1', + assetType: AssetType.OFFICE, + resultType: ResultType.VERIFIED_PORTFOLIO, + location: { city: 'Zürich', district: 'Kreis 5', canton: 'ZH', country: 'CH', coordinates: { lat: 47.382, lng: 8.521 } }, + address: { street: 'Technoparkstrasse', houseNumber: '1', postalCode: '8005', city: 'Zürich', country: 'CH' }, + areaSqm: 148, + rentPricePerSqm: 268, + totalRentMonthly: 39664, + availabilityDate: '2026-07-01', + availabilityStatus: AvailabilityStatus.AVAILABLE_SOON, + sourceType: 'ERP_IMPORT', + confidenceScore: 0.93, + dataQuality: { + score: 0.90, + missingCriticalFields: [], + missingOptionalFields: [], + lastVerifiedAt: '2025-05-15', + freshness: DataFreshness.FRESH, + warnings: [], + }, + softFactors: { + prestige: 65, + accessibility: 86, + visibilityScore: 55, + talentAccess: 85, + parkingSpots: 3, + publicTransportMinutes: 5, + }, + hardFacts: { floor: 3, parking: 3, fitOut: 'BASIC', ceilingHeightM: 3.0 }, + floorLevel: 3, + contractDurationMonths: 84, + ancillaryCosts: 4.0, + riskLevel: RiskLevel.LOW, + images: ['https://images.unsplash.com/photo-1497366216548-37526070297c?w=800&h=400&fit=crop'], + mapImageUrl: 'https://images.unsplash.com/photo-1524661135-423995f22d0b?w=600&h=300&fit=crop', + propertyNumber: 'ZH-2023-066', + description: 'Bürofläche im Technopark, Escher-Wyss-Platz, Zürich Kreis 5. 148 m² im 3. OG, Basisausbau, exzellenter Talent-Zugang im Tech-Umfeld. 3 Parkplätze, direkte Tramanbindung. Verfügbar Juli 2026.', + units: [ + { id: 'unit-066-1', propertyId: 'prop-066', floorLevel: 3, unitLabel: '3.OG', areaSqm: 148, available: true, rentPricePerSqm: 268 }, + ], + importedFrom: 'SAP RE-FX', + importedAt: '2025-01-15T08:00:00Z', + lastUpdatedAt: '2025-05-15T10:00:00Z', + breakoutOption: false, + schattenmarktRelease: { enabled: false }, + organizationId: 'org-wincasa', + createdAt: '2025-05-22T10:00:00Z', + updatedAt: '2025-05-22T10:00:00Z', + }, + + { + id: 'prop-067', + title: 'Bürofläche Altstetterstrasse 222', + assetType: AssetType.OFFICE, + resultType: ResultType.EXTERNAL_MARKET, + location: { city: 'Zürich', district: 'Altstetten', canton: 'ZH', country: 'CH', coordinates: { lat: 47.385, lng: 8.488 } }, + address: { street: 'Altstetterstrasse', houseNumber: '222', postalCode: '8048', city: 'Zürich', country: 'CH' }, + areaSqm: 190, + rentPricePerSqm: 238, + totalRentMonthly: 45220, + availabilityDate: '2026-06-01', + availabilityStatus: AvailabilityStatus.AVAILABLE_NOW, + sourceType: 'HOMEGATE_SCRAPE', + sourceUrl: 'https://example.com/listing/prop-067', + confidenceScore: 0.69, + dataQuality: { + score: 0.64, + missingCriticalFields: ['contractDurationMonths'], + missingOptionalFields: ['ancillaryCosts'], + lastVerifiedAt: '2025-05-08', + freshness: DataFreshness.STALE, + warnings: ['Daten aus Drittquelle', 'Ausserhalb Zieldistrikt UC-B'], + }, + softFactors: { + prestige: 48, + accessibility: 78, + visibilityScore: 38, + talentAccess: 62, + parkingSpots: 8, + publicTransportMinutes: 10, + }, + hardFacts: { floor: 1, parking: 8, fitOut: 'BASIC', ceilingHeightM: 2.7 }, + floorLevel: 1, + contractDurationMonths: 60, + ancillaryCosts: 3.5, + riskLevel: RiskLevel.MEDIUM, + images: ['https://images.unsplash.com/photo-1522071820081-009f0129c71c?w=800&h=400&fit=crop'], + mapImageUrl: 'https://images.unsplash.com/photo-1524661135-423995f22d0b?w=600&h=300&fit=crop', + propertyNumber: 'ZH-EXT-067', + description: 'Bürofläche im 1. OG, Altstetterstrasse. 190 m², Basisausbau, günstiger Mietpreis. Ausserhalb des gewünschten Distrikts, aber niedrige Kosten als Kompromissoption.', + units: [ + { id: 'unit-067-1', propertyId: 'prop-067', floorLevel: 1, areaSqm: 190, available: true, rentPricePerSqm: 238 }, + ], + createdAt: '2025-05-08T08:00:00Z', + updatedAt: '2025-05-08T08:00:00Z', + }, + + { + id: 'prop-068', + title: 'Bürofläche Limmatstrasse 264', + assetType: AssetType.OFFICE, + resultType: ResultType.VERIFIED_PORTFOLIO, + location: { city: 'Zürich', district: 'Zürich-West', canton: 'ZH', country: 'CH', coordinates: { lat: 47.388, lng: 8.510 } }, + address: { street: 'Limmatstrasse', houseNumber: '264', postalCode: '8005', city: 'Zürich', country: 'CH' }, + areaSqm: 160, + rentPricePerSqm: 282, + totalRentMonthly: 45120, + availabilityDate: '2025-12-01', + availabilityStatus: AvailabilityStatus.AVAILABLE_SOON, + sourceType: 'ERP_IMPORT', + confidenceScore: 0.94, + dataQuality: { + score: 0.91, + missingCriticalFields: [], + missingOptionalFields: [], + lastVerifiedAt: '2025-05-15', + freshness: DataFreshness.FRESH, + warnings: [], + }, + softFactors: { + prestige: 64, + accessibility: 84, + visibilityScore: 52, + talentAccess: 80, + parkingSpots: 2, + publicTransportMinutes: 5, + }, + hardFacts: { floor: 2, parking: 2, fitOut: 'BASIC', ceilingHeightM: 3.2 }, + floorLevel: 2, + contractDurationMonths: 84, + ancillaryCosts: 4.0, + riskLevel: RiskLevel.LOW, + images: ['https://images.unsplash.com/photo-1497366216548-37526070297c?w=800&h=400&fit=crop'], + mapImageUrl: 'https://images.unsplash.com/photo-1524661135-423995f22d0b?w=600&h=300&fit=crop', + propertyNumber: 'ZH-2023-068', + description: 'Bürofläche im Viadukt-Quartier, Limmatstrasse 264, Zürich-West. 160 m² im 2. OG, Basisausbau mit Industriecharme. 2 Parkplätze, Tram vor dem Haus. Verfügbar Dezember 2025.', + units: [ + { id: 'unit-068-1', propertyId: 'prop-068', floorLevel: 2, unitLabel: '2.OG', areaSqm: 160, available: true, rentPricePerSqm: 282 }, + ], + importedFrom: 'SAP RE-FX', + importedAt: '2025-01-15T08:00:00Z', + lastUpdatedAt: '2025-05-15T10:00:00Z', + breakoutOption: false, + schattenmarktRelease: { enabled: false }, + organizationId: 'org-wincasa', + createdAt: '2025-05-22T10:00:00Z', + updatedAt: '2025-05-22T10:00:00Z', + }, + + { + id: 'prop-069', + title: 'Bürofläche Pfingstweidstrasse 20 (Signal: Auszug)', + assetType: AssetType.OFFICE, + resultType: ResultType.FUTURE_AVAILABILITY, + location: { city: 'Zürich', district: 'Zürich-West', canton: 'ZH', country: 'CH', coordinates: { lat: 47.388, lng: 8.510 } }, + address: { street: 'Pfingstweidstrasse', houseNumber: '20', postalCode: '8005', city: 'Zürich', country: 'CH' }, + areaSqm: 172, + rentPricePerSqm: 278, + availabilityDate: '2026-10-01', + availabilityStatus: AvailabilityStatus.AVAILABLE_SOON, + sourceType: 'ERP_IMPORT', + confidenceScore: 0.80, + dataQuality: { + score: 0.75, + missingCriticalFields: [], + missingOptionalFields: ['expansionPotentialSqm'], + lastVerifiedAt: '2025-05-15', + freshness: DataFreshness.FRESH, + warnings: ['Aktuell vermietet — Pre-Market-Freigabe aktiv'], + }, + softFactors: { + prestige: 62, + accessibility: 85, + visibilityScore: 50, + talentAccess: 80, + parkingSpots: 4, + publicTransportMinutes: 6, + }, + hardFacts: { floor: 2, parking: 4, fitOut: 'BASIC', ceilingHeightM: 3.0 }, + floorLevel: 2, + contractDurationMonths: 84, + ancillaryCosts: 4.0, + riskLevel: RiskLevel.LOW, + images: ['https://images.unsplash.com/photo-1497366216548-37526070297c?w=800&h=400&fit=crop'], + mapImageUrl: 'https://images.unsplash.com/photo-1524661135-423995f22d0b?w=600&h=300&fit=crop', + propertyNumber: 'ZH-2024-069', + description: 'Bürofläche Pfingstweidstrasse 20, Zürich-West. Derzeit von Kreativagentur GmbH belegt — Auszug Oktober 2026 bekannt. Pre-Market-Freigabe aktiv. 172 m², 2. OG, Basisausbau.', + units: [ + { id: 'unit-069-1', propertyId: 'prop-069', floorLevel: 2, unitLabel: '2.OG', areaSqm: 172, available: false, rentPricePerSqm: 278, currentTenant: 'Kreativagentur GmbH', leaseEndDate: '2026-10-01', schattenmarktRelease: { enabled: true, availableFrom: '2026-10-01' } }, + ], + importedFrom: 'SAP RE-FX', + importedAt: '2025-01-15T08:00:00Z', + lastUpdatedAt: '2025-05-15T10:00:00Z', + currentTenant: 'Kreativagentur GmbH', + leaseEndDate: '2026-10-01', + breakoutOption: false, + schattenmarktRelease: { enabled: true, leadTimeMonths: 6 }, + organizationId: 'org-wincasa', + createdAt: '2025-05-22T10:00:00Z', + updatedAt: '2025-05-22T10:00:00Z', + }, + + // ── UC-C extras: Premium Office Zürich Kreis 1 / Seefeld ───────────────────── + + { + id: 'prop-070', + title: 'Bürofläche Talstrasse 50', + assetType: AssetType.OFFICE, + resultType: ResultType.VERIFIED_PORTFOLIO, + location: { city: 'Zürich', district: 'Kreis 1', canton: 'ZH', country: 'CH', coordinates: { lat: 47.376, lng: 8.541 } }, + address: { street: 'Talstrasse', houseNumber: '50', postalCode: '8001', city: 'Zürich', country: 'CH' }, + areaSqm: 505, + rentPricePerSqm: 842, + totalRentMonthly: 425210, + availabilityDate: '2026-07-01', + availabilityStatus: AvailabilityStatus.AVAILABLE_SOON, + sourceType: 'ERP_IMPORT', + confidenceScore: 0.97, + dataQuality: { + score: 0.95, + missingCriticalFields: [], + missingOptionalFields: [], + lastVerifiedAt: '2025-05-15', + freshness: DataFreshness.FRESH, + warnings: [], + }, + softFactors: { + prestige: 97, + accessibility: 98, + visibilityScore: 96, + talentAccess: 95, + parkingSpots: 5, + publicTransportMinutes: 2, + }, + hardFacts: { floor: 4, parking: 5, hasAirConditioning: true, fitOut: 'PREMIUM', ceilingHeightM: 3.0, isBarrierFree: true }, + floorLevel: 4, + contractDurationMonths: 120, + ancillaryCosts: 9.5, + riskLevel: RiskLevel.LOW, + images: ['https://images.unsplash.com/photo-1560472355-536de3962603?w=800&h=400&fit=crop'], + mapImageUrl: 'https://images.unsplash.com/photo-1524661135-423995f22d0b?w=600&h=300&fit=crop', + propertyNumber: 'ZH-2022-070', + description: 'Exklusive Büroetage an der Talstrasse, Zürich Kreis 1. 505 m² im 4. OG, PREMIUM-Ausbau, Klimaanlage. Repräsentativer Eingangsbereich, Panoramablick. Idealer Standort für Finanzdienstleister und Family Offices. Verfügbar Juli 2026.', + units: [ + { id: 'unit-070-1', propertyId: 'prop-070', floorLevel: 4, unitLabel: '4.OG', areaSqm: 505, available: true, rentPricePerSqm: 842 }, + ], + importedFrom: 'SAP RE-FX', + importedAt: '2025-01-15T08:00:00Z', + lastUpdatedAt: '2025-05-15T10:00:00Z', + breakoutOption: false, + schattenmarktRelease: { enabled: false }, + organizationId: 'org-wincasa', + createdAt: '2025-05-22T10:00:00Z', + updatedAt: '2025-05-22T10:00:00Z', + }, + + { + id: 'prop-071', + title: 'Bürofläche Bleicherweg 48', + assetType: AssetType.OFFICE, + resultType: ResultType.VERIFIED_PORTFOLIO, + location: { city: 'Zürich', district: 'Kreis 1', canton: 'ZH', country: 'CH', coordinates: { lat: 47.376, lng: 8.541 } }, + address: { street: 'Bleicherweg', houseNumber: '48', postalCode: '8001', city: 'Zürich', country: 'CH' }, + areaSqm: 485, + rentPricePerSqm: 868, + totalRentMonthly: 420980, + availabilityDate: '2026-04-01', + availabilityStatus: AvailabilityStatus.AVAILABLE_SOON, + sourceType: 'ERP_IMPORT', + confidenceScore: 0.96, + dataQuality: { + score: 0.94, + missingCriticalFields: [], + missingOptionalFields: [], + lastVerifiedAt: '2025-05-15', + freshness: DataFreshness.FRESH, + warnings: [], + }, + softFactors: { + prestige: 96, + accessibility: 97, + visibilityScore: 94, + talentAccess: 94, + parkingSpots: 4, + publicTransportMinutes: 2, + }, + hardFacts: { floor: 5, parking: 4, hasAirConditioning: true, fitOut: 'PREMIUM', ceilingHeightM: 3.0, isBarrierFree: true }, + floorLevel: 5, + contractDurationMonths: 120, + ancillaryCosts: 9.5, + riskLevel: RiskLevel.LOW, + images: ['https://images.unsplash.com/photo-1560472355-536de3962603?w=800&h=400&fit=crop'], + mapImageUrl: 'https://images.unsplash.com/photo-1524661135-423995f22d0b?w=600&h=300&fit=crop', + propertyNumber: 'ZH-2022-071', + description: 'Prestigiöse Büroetage am Bleicherweg, Zürich Kreis 1. 485 m² im 5. OG, PREMIUM-Ausbau, Klimaanlage, Seeblick. Erstklassige Adresse für Vermögensverwalter und Finanzinstitute. Verfügbar April 2026.', + units: [ + { id: 'unit-071-1', propertyId: 'prop-071', floorLevel: 5, unitLabel: '5.OG', areaSqm: 485, available: true, rentPricePerSqm: 868 }, + ], + importedFrom: 'SAP RE-FX', + importedAt: '2025-01-15T08:00:00Z', + lastUpdatedAt: '2025-05-15T10:00:00Z', + breakoutOption: false, + schattenmarktRelease: { enabled: false }, + organizationId: 'org-wincasa', + createdAt: '2025-05-22T10:00:00Z', + updatedAt: '2025-05-22T10:00:00Z', + }, + + { + id: 'prop-072', + title: 'Bürofläche Utoquai 34', + assetType: AssetType.OFFICE, + resultType: ResultType.VERIFIED_PORTFOLIO, + location: { city: 'Zürich', district: 'Seefeld', canton: 'ZH', country: 'CH', coordinates: { lat: 47.356, lng: 8.550 } }, + address: { street: 'Utoquai', houseNumber: '34', postalCode: '8008', city: 'Zürich', country: 'CH' }, + areaSqm: 525, + rentPricePerSqm: 808, + totalRentMonthly: 424200, + availabilityDate: '2026-10-01', + availabilityStatus: AvailabilityStatus.AVAILABLE_SOON, + sourceType: 'ERP_IMPORT', + confidenceScore: 0.95, + dataQuality: { + score: 0.93, + missingCriticalFields: [], + missingOptionalFields: [], + lastVerifiedAt: '2025-05-15', + freshness: DataFreshness.FRESH, + warnings: [], + }, + softFactors: { + prestige: 93, + accessibility: 96, + visibilityScore: 90, + talentAccess: 92, + parkingSpots: 4, + publicTransportMinutes: 3, + }, + hardFacts: { floor: 3, parking: 4, hasAirConditioning: true, fitOut: 'PREMIUM', ceilingHeightM: 3.0, isBarrierFree: true }, + floorLevel: 3, + contractDurationMonths: 120, + ancillaryCosts: 9.0, + riskLevel: RiskLevel.LOW, + images: ['https://images.unsplash.com/photo-1560472355-536de3962603?w=800&h=400&fit=crop'], + mapImageUrl: 'https://images.unsplash.com/photo-1524661135-423995f22d0b?w=600&h=300&fit=crop', + propertyNumber: 'ZH-2022-072', + description: 'Exklusive Büroetage am Utoquai, Zürich Seefeld. 525 m² im 3. OG mit direktem Seeblick, PREMIUM-Ausbau, Klimaanlage. Toplage für internationale Gesellschaften und Wealth-Management-Firmen. Verfügbar Oktober 2026.', + units: [ + { id: 'unit-072-1', propertyId: 'prop-072', floorLevel: 3, unitLabel: '3.OG Seeblick', areaSqm: 525, available: true, rentPricePerSqm: 808 }, + ], + importedFrom: 'SAP RE-FX', + importedAt: '2025-01-15T08:00:00Z', + lastUpdatedAt: '2025-05-15T10:00:00Z', + breakoutOption: false, + schattenmarktRelease: { enabled: false }, + organizationId: 'org-wincasa', + createdAt: '2025-05-22T10:00:00Z', + updatedAt: '2025-05-22T10:00:00Z', + }, + + { + id: 'prop-073', + title: 'Bürofläche Rämistrasse 45', + assetType: AssetType.OFFICE, + resultType: ResultType.VERIFIED_PORTFOLIO, + location: { city: 'Zürich', district: 'Seefeld', canton: 'ZH', country: 'CH', coordinates: { lat: 47.356, lng: 8.550 } }, + address: { street: 'Rämistrasse', houseNumber: '45', postalCode: '8008', city: 'Zürich', country: 'CH' }, + areaSqm: 468, + rentPricePerSqm: 788, + totalRentMonthly: 368784, + availabilityDate: '2026-01-01', + availabilityStatus: AvailabilityStatus.AVAILABLE_SOON, + sourceType: 'ERP_IMPORT', + confidenceScore: 0.95, + dataQuality: { + score: 0.92, + missingCriticalFields: [], + missingOptionalFields: [], + lastVerifiedAt: '2025-05-15', + freshness: DataFreshness.FRESH, + warnings: [], + }, + softFactors: { + prestige: 91, + accessibility: 95, + visibilityScore: 88, + talentAccess: 90, + parkingSpots: 3, + publicTransportMinutes: 3, + }, + hardFacts: { floor: 2, parking: 3, hasAirConditioning: true, fitOut: 'PREMIUM', ceilingHeightM: 3.0, isBarrierFree: true }, + floorLevel: 2, + contractDurationMonths: 120, + ancillaryCosts: 9.0, + riskLevel: RiskLevel.LOW, + images: ['https://images.unsplash.com/photo-1560472355-536de3962603?w=800&h=400&fit=crop'], + mapImageUrl: 'https://images.unsplash.com/photo-1524661135-423995f22d0b?w=600&h=300&fit=crop', + propertyNumber: 'ZH-2022-073', + description: 'Repräsentative Büroetage an der Rämistrasse, Zürich Seefeld. 468 m² im 2. OG, PREMIUM-Ausbau mit Klimaanlage. Ruhige, elegante Umgebung nahe Zürichsee. Verfügbar Januar 2026.', + units: [ + { id: 'unit-073-1', propertyId: 'prop-073', floorLevel: 2, unitLabel: '2.OG', areaSqm: 468, available: true, rentPricePerSqm: 788 }, + ], + importedFrom: 'SAP RE-FX', + importedAt: '2025-01-15T08:00:00Z', + lastUpdatedAt: '2025-05-15T10:00:00Z', + breakoutOption: false, + schattenmarktRelease: { enabled: false }, + organizationId: 'org-wincasa', + createdAt: '2025-05-22T10:00:00Z', + updatedAt: '2025-05-22T10:00:00Z', + }, + + { + id: 'prop-074', + title: 'Bürofläche Uraniastrasse 14', + assetType: AssetType.OFFICE, + resultType: ResultType.EXTERNAL_MARKET, + location: { city: 'Zürich', district: 'Kreis 1', canton: 'ZH', country: 'CH', coordinates: { lat: 47.376, lng: 8.541 } }, + address: { street: 'Uraniastrasse', houseNumber: '14', postalCode: '8001', city: 'Zürich', country: 'CH' }, + areaSqm: 510, + rentPricePerSqm: 878, + totalRentMonthly: 447780, + availabilityDate: '2026-06-01', + availabilityStatus: AvailabilityStatus.AVAILABLE_NOW, + sourceType: 'IMMOSCOUT_SCRAPE', + sourceUrl: 'https://example.com/listing/prop-074', + confidenceScore: 0.74, + dataQuality: { + score: 0.70, + missingCriticalFields: ['contractDurationMonths'], + missingOptionalFields: ['ancillaryCosts'], + lastVerifiedAt: '2025-05-10', + freshness: DataFreshness.STALE, + warnings: ['Daten aus Drittquelle'], + }, + softFactors: { + prestige: 95, + accessibility: 97, + visibilityScore: 90, + talentAccess: 93, + parkingSpots: 4, + publicTransportMinutes: 2, + }, + hardFacts: { floor: 4, parking: 4, hasAirConditioning: true, fitOut: 'PREMIUM', ceilingHeightM: 3.0, isBarrierFree: true }, + floorLevel: 4, + contractDurationMonths: 120, + ancillaryCosts: 9.0, + riskLevel: RiskLevel.MEDIUM, + images: ['https://images.unsplash.com/photo-1560472355-536de3962603?w=800&h=400&fit=crop'], + mapImageUrl: 'https://images.unsplash.com/photo-1524661135-423995f22d0b?w=600&h=300&fit=crop', + propertyNumber: 'ZH-EXT-074', + description: 'Premium-Bürofläche an der Uraniastrasse, Zürich Kreis 1. 510 m², 4. OG, Klimaanlage, PREMIUM-Ausbau. Sehr gute Adresse nahe Bahnhofstrasse.', + units: [ + { id: 'unit-074-1', propertyId: 'prop-074', floorLevel: 4, areaSqm: 510, available: true, rentPricePerSqm: 878 }, + ], + createdAt: '2025-05-10T08:00:00Z', + updatedAt: '2025-05-10T08:00:00Z', + }, + + + { + id: 'prop-075', + title: 'Bürofläche Seefeldstrasse 30', + assetType: AssetType.OFFICE, + resultType: ResultType.EXTERNAL_MARKET, + location: { city: 'Zürich', district: 'Seefeld', canton: 'ZH', country: 'CH', coordinates: { lat: 47.356, lng: 8.550 } }, + address: { street: 'Seefeldstrasse', houseNumber: '30', postalCode: '8008', city: 'Zürich', country: 'CH' }, + areaSqm: 540, + rentPricePerSqm: 818, + totalRentMonthly: 441720, + availabilityDate: '2026-06-01', + availabilityStatus: AvailabilityStatus.AVAILABLE_NOW, + sourceType: 'HOMEGATE_SCRAPE', + sourceUrl: 'https://example.com/listing/prop-075', + confidenceScore: 0.72, + dataQuality: { + score: 0.68, + missingCriticalFields: ['contractDurationMonths'], + missingOptionalFields: ['ancillaryCosts'], + lastVerifiedAt: '2025-05-10', + freshness: DataFreshness.STALE, + warnings: ['Daten aus Drittquelle', 'Klimaanlage nicht bestätigt'], + }, + softFactors: { + prestige: 92, + accessibility: 94, + visibilityScore: 88, + talentAccess: 91, + parkingSpots: 4, + publicTransportMinutes: 3, + }, + hardFacts: { floor: 3, parking: 4, fitOut: 'PREMIUM', ceilingHeightM: 3.0, isBarrierFree: true }, + floorLevel: 3, + contractDurationMonths: 120, + ancillaryCosts: 9.0, + riskLevel: RiskLevel.MEDIUM, + images: ['https://images.unsplash.com/photo-1560472355-536de3962603?w=800&h=400&fit=crop'], + mapImageUrl: 'https://images.unsplash.com/photo-1524661135-423995f22d0b?w=600&h=300&fit=crop', + propertyNumber: 'ZH-EXT-075', + description: 'Premium-Bürofläche an der Seefeldstrasse, Zürich Seefeld. 540 m², 3. OG, PREMIUM-Ausbau. Klimaanlage gemäss Inserat, Bestätigung ausstehend.', + units: [ + { id: 'unit-075-1', propertyId: 'prop-075', floorLevel: 3, areaSqm: 540, available: true, rentPricePerSqm: 818 }, + ], + createdAt: '2025-05-10T08:00:00Z', + updatedAt: '2025-05-10T08:00:00Z', + }, + + { + id: 'prop-076', + title: 'Bürofläche Stockerstrasse 30', + assetType: AssetType.OFFICE, + resultType: ResultType.VERIFIED_PORTFOLIO, + location: { city: 'Zürich', district: 'Kreis 1', canton: 'ZH', country: 'CH', coordinates: { lat: 47.376, lng: 8.541 } }, + address: { street: 'Stockerstrasse', houseNumber: '30', postalCode: '8001', city: 'Zürich', country: 'CH' }, + areaSqm: 495, + rentPricePerSqm: 858, + totalRentMonthly: 424710, + availabilityDate: '2026-04-01', + availabilityStatus: AvailabilityStatus.AVAILABLE_SOON, + sourceType: 'ERP_IMPORT', + confidenceScore: 0.97, + dataQuality: { + score: 0.94, + missingCriticalFields: [], + missingOptionalFields: [], + lastVerifiedAt: '2025-05-15', + freshness: DataFreshness.FRESH, + warnings: [], + }, + softFactors: { + prestige: 96, + accessibility: 97, + visibilityScore: 94, + talentAccess: 94, + parkingSpots: 5, + publicTransportMinutes: 2, + }, + hardFacts: { floor: 6, parking: 5, hasAirConditioning: true, fitOut: 'PREMIUM', ceilingHeightM: 3.0, isBarrierFree: true }, + floorLevel: 6, + contractDurationMonths: 120, + ancillaryCosts: 9.5, + riskLevel: RiskLevel.LOW, + images: ['https://images.unsplash.com/photo-1560472355-536de3962603?w=800&h=400&fit=crop'], + mapImageUrl: 'https://images.unsplash.com/photo-1524661135-423995f22d0b?w=600&h=300&fit=crop', + propertyNumber: 'ZH-2022-076', + description: 'Exklusive Penthouse-Büroetage an der Stockerstrasse, Zürich Kreis 1. 495 m² im 6. OG, PREMIUM-Ausbau, Klimaanlage, Terrasse. Ideale Adresse für Wealth-Management und Private Equity. Verfügbar April 2026.', + units: [ + { id: 'unit-076-1', propertyId: 'prop-076', floorLevel: 6, unitLabel: '6.OG', areaSqm: 495, available: true, rentPricePerSqm: 858 }, + ], + importedFrom: 'SAP RE-FX', + importedAt: '2025-01-15T08:00:00Z', + lastUpdatedAt: '2025-05-15T10:00:00Z', + breakoutOption: false, + schattenmarktRelease: { enabled: false }, + organizationId: 'org-wincasa', + createdAt: '2025-05-22T10:00:00Z', + updatedAt: '2025-05-22T10:00:00Z', + }, + + { + id: 'prop-077', + title: 'Bürofläche Löwenstrasse 16 (Signal: Auszug)', + assetType: AssetType.OFFICE, + resultType: ResultType.FUTURE_AVAILABILITY, + location: { city: 'Zürich', district: 'Kreis 1', canton: 'ZH', country: 'CH', coordinates: { lat: 47.376, lng: 8.541 } }, + address: { street: 'Löwenstrasse', houseNumber: '16', postalCode: '8001', city: 'Zürich', country: 'CH' }, + areaSqm: 515, + rentPricePerSqm: 832, + availabilityDate: '2026-09-01', + availabilityStatus: AvailabilityStatus.AVAILABLE_SOON, + sourceType: 'ERP_IMPORT', + confidenceScore: 0.85, + dataQuality: { + score: 0.82, + missingCriticalFields: [], + missingOptionalFields: ['expansionPotentialSqm'], + lastVerifiedAt: '2025-05-15', + freshness: DataFreshness.FRESH, + warnings: ['Aktuell vermietet — Pre-Market-Freigabe aktiv'], + }, + softFactors: { + prestige: 95, + accessibility: 97, + visibilityScore: 93, + talentAccess: 93, + parkingSpots: 4, + publicTransportMinutes: 2, + }, + hardFacts: { floor: 4, parking: 4, hasAirConditioning: true, fitOut: 'PREMIUM', ceilingHeightM: 3.0, isBarrierFree: true }, + floorLevel: 4, + contractDurationMonths: 120, + ancillaryCosts: 9.5, + riskLevel: RiskLevel.LOW, + images: ['https://images.unsplash.com/photo-1560472355-536de3962603?w=800&h=400&fit=crop'], + mapImageUrl: 'https://images.unsplash.com/photo-1524661135-423995f22d0b?w=600&h=300&fit=crop', + propertyNumber: 'ZH-2024-077', + description: 'Bürofläche Löwenstrasse 16, Zürich Kreis 1. Derzeit von Private Equity GmbH belegt — Auszug September 2026 bekannt. Pre-Market-Freigabe aktiv. 515 m², 4. OG, PREMIUM-Ausbau, Klimaanlage.', + units: [ + { id: 'unit-077-1', propertyId: 'prop-077', floorLevel: 4, unitLabel: '4.OG', areaSqm: 515, available: false, rentPricePerSqm: 832, currentTenant: 'Private Equity GmbH', leaseEndDate: '2026-09-01', schattenmarktRelease: { enabled: true, availableFrom: '2026-09-01' } }, + ], + importedFrom: 'SAP RE-FX', + importedAt: '2025-01-15T08:00:00Z', + lastUpdatedAt: '2025-05-15T10:00:00Z', + currentTenant: 'Private Equity GmbH', + leaseEndDate: '2026-09-01', + breakoutOption: false, + schattenmarktRelease: { enabled: true, leadTimeMonths: 6 }, + organizationId: 'org-wincasa', + createdAt: '2025-05-22T10:00:00Z', + updatedAt: '2025-05-22T10:00:00Z', + }, + + // ── UC-E extras: Retail Bern Innenstadt ────────────────────────────────────── + + { + id: 'prop-078', + title: 'Retailfläche Kramgasse 18', + assetType: AssetType.RETAIL, + resultType: ResultType.VERIFIED_PORTFOLIO, + location: { city: 'Bern', district: 'Innenstadt', canton: 'BE', country: 'CH', coordinates: { lat: 46.948, lng: 7.447 } }, + address: { street: 'Kramgasse', houseNumber: '18', postalCode: '3011', city: 'Bern', country: 'CH' }, + areaSqm: 250, + rentPricePerSqm: 1148, + totalRentMonthly: 287000, + availabilityDate: '2026-07-01', + availabilityStatus: AvailabilityStatus.AVAILABLE_SOON, + sourceType: 'ERP_IMPORT', + confidenceScore: 0.94, + dataQuality: { + score: 0.92, + missingCriticalFields: [], + missingOptionalFields: [], + lastVerifiedAt: '2025-05-15', + freshness: DataFreshness.FRESH, + warnings: [], + }, + softFactors: { + prestige: 88, + accessibility: 94, + visibilityScore: 90, + passerbyFrequency: 'HIGH', + talentAccess: 78, + parkingSpots: 0, + publicTransportMinutes: 3, + }, + hardFacts: { floor: 0, parking: 0, hasStorefront: true, fitOut: 'FULL', isBarrierFree: true }, + floorLevel: 0, + contractDurationMonths: 60, + ancillaryCosts: 7.0, + riskLevel: RiskLevel.LOW, + images: ['https://images.unsplash.com/photo-1441986300917-64674bd600d8?w=800&h=400&fit=crop'], + mapImageUrl: 'https://images.unsplash.com/photo-1524661135-423995f22d0b?w=600&h=300&fit=crop', + propertyNumber: 'BE-2024-078', + description: 'Erstklassige Retailfläche in der Kramgasse, Bern Altstadt. EG mit Schaufensterfront, hohe Fussgängerfrequenz in der historischen Einkaufsmeile. Vollständig ausgebaut. Verfügbar Juli 2026.', + units: [ + { id: 'unit-078-1', propertyId: 'prop-078', floorLevel: 0, unitLabel: 'EG Laden', areaSqm: 250, available: true, rentPricePerSqm: 1148 }, + ], + importedFrom: 'SAP RE-FX', + importedAt: '2025-01-15T08:00:00Z', + lastUpdatedAt: '2025-05-15T10:00:00Z', + breakoutOption: false, + schattenmarktRelease: { enabled: false }, + organizationId: 'org-wincasa', + createdAt: '2025-05-22T10:00:00Z', + updatedAt: '2025-05-22T10:00:00Z', + }, + + { + id: 'prop-079', + title: 'Retailfläche Marktgasse 42', + assetType: AssetType.RETAIL, + resultType: ResultType.VERIFIED_PORTFOLIO, + location: { city: 'Bern', district: 'Innenstadt', canton: 'BE', country: 'CH', coordinates: { lat: 46.948, lng: 7.447 } }, + address: { street: 'Marktgasse', houseNumber: '42', postalCode: '3011', city: 'Bern', country: 'CH' }, + areaSqm: 310, + rentPricePerSqm: 1078, + totalRentMonthly: 334180, + availabilityDate: '2026-10-01', + availabilityStatus: AvailabilityStatus.AVAILABLE_SOON, + sourceType: 'ERP_IMPORT', + confidenceScore: 0.93, + dataQuality: { + score: 0.91, + missingCriticalFields: [], + missingOptionalFields: [], + lastVerifiedAt: '2025-05-15', + freshness: DataFreshness.FRESH, + warnings: [], + }, + softFactors: { + prestige: 85, + accessibility: 95, + visibilityScore: 88, + passerbyFrequency: 'MEDIUM_HIGH', + talentAccess: 76, + parkingSpots: 0, + publicTransportMinutes: 3, + }, + hardFacts: { floor: 0, parking: 0, hasStorefront: true, fitOut: 'FULL', isBarrierFree: true }, + floorLevel: 0, + contractDurationMonths: 60, + ancillaryCosts: 7.0, + riskLevel: RiskLevel.LOW, + images: ['https://images.unsplash.com/photo-1555529669-e69e7aa0ba9a?w=800&h=400&fit=crop'], + mapImageUrl: 'https://images.unsplash.com/photo-1524661135-423995f22d0b?w=600&h=300&fit=crop', + propertyNumber: 'BE-2024-079', + description: 'Grosszügige Retailfläche an der Marktgasse, Bern Innenstadt. 310 m² im EG, Schaufensterfront, vollständig ausgebaut. Starke Passantenfrequenz, Tram direkt vor dem Haus. Verfügbar Oktober 2026.', + units: [ + { id: 'unit-079-1', propertyId: 'prop-079', floorLevel: 0, unitLabel: 'EG Laden', areaSqm: 310, available: true, rentPricePerSqm: 1078 }, + ], + importedFrom: 'SAP RE-FX', + importedAt: '2025-01-15T08:00:00Z', + lastUpdatedAt: '2025-05-15T10:00:00Z', + breakoutOption: false, + schattenmarktRelease: { enabled: false }, + organizationId: 'org-wincasa', + createdAt: '2025-05-22T10:00:00Z', + updatedAt: '2025-05-22T10:00:00Z', + }, + + { + id: 'prop-080', + title: 'Ladenfläche Spitalgasse 34', + assetType: AssetType.RETAIL, + resultType: ResultType.EXTERNAL_MARKET, + location: { city: 'Bern', district: 'Innenstadt', canton: 'BE', country: 'CH', coordinates: { lat: 46.948, lng: 7.447 } }, + address: { street: 'Spitalgasse', houseNumber: '34', postalCode: '3011', city: 'Bern', country: 'CH' }, + areaSqm: 280, + rentPricePerSqm: 1318, + totalRentMonthly: 369040, + availabilityDate: '2026-06-01', + availabilityStatus: AvailabilityStatus.AVAILABLE_NOW, + sourceType: 'IMMOSCOUT_SCRAPE', + sourceUrl: 'https://example.com/listing/prop-080', + confidenceScore: 0.72, + dataQuality: { + score: 0.68, + missingCriticalFields: ['contractDurationMonths'], + missingOptionalFields: ['ancillaryCosts'], + lastVerifiedAt: '2025-05-10', + freshness: DataFreshness.STALE, + warnings: ['Daten aus Drittquelle'], + }, + softFactors: { + prestige: 86, + accessibility: 95, + visibilityScore: 90, + passerbyFrequency: 'HIGH', + talentAccess: 75, + parkingSpots: 0, + publicTransportMinutes: 3, + }, + hardFacts: { floor: 0, parking: 0, hasStorefront: true, fitOut: 'FULL', isBarrierFree: true }, + floorLevel: 0, + contractDurationMonths: 60, + ancillaryCosts: 7.5, + riskLevel: RiskLevel.MEDIUM, + images: ['https://images.unsplash.com/photo-1528698827591-e19ccd7bc23d?w=800&h=400&fit=crop'], + mapImageUrl: 'https://images.unsplash.com/photo-1524661135-423995f22d0b?w=600&h=300&fit=crop', + propertyNumber: 'BE-EXT-080', + description: 'Ladenfläche an der Spitalgasse, Bern Innenstadt. EG, Schaufensterfront, hohe Passantenfrequenz. Vollausbau gemäss Inserat.', + units: [ + { id: 'unit-080-1', propertyId: 'prop-080', floorLevel: 0, areaSqm: 280, available: true, rentPricePerSqm: 1318 }, + ], + createdAt: '2025-05-10T08:00:00Z', + updatedAt: '2025-05-10T08:00:00Z', + }, + + { + id: 'prop-081', + title: 'Ladenfläche Münstergasse 12', + assetType: AssetType.RETAIL, + resultType: ResultType.EXTERNAL_MARKET, + location: { city: 'Bern', district: 'Innenstadt', canton: 'BE', country: 'CH', coordinates: { lat: 46.948, lng: 7.447 } }, + address: { street: 'Münstergasse', houseNumber: '12', postalCode: '3011', city: 'Bern', country: 'CH' }, + areaSqm: 350, + rentPricePerSqm: 1198, + totalRentMonthly: 419300, + availabilityDate: '2026-06-01', + availabilityStatus: AvailabilityStatus.AVAILABLE_NOW, + sourceType: 'HOMEGATE_SCRAPE', + sourceUrl: 'https://example.com/listing/prop-081', + confidenceScore: 0.70, + dataQuality: { + score: 0.66, + missingCriticalFields: ['contractDurationMonths'], + missingOptionalFields: ['ancillaryCosts'], + lastVerifiedAt: '2025-05-08', + freshness: DataFreshness.STALE, + warnings: ['Daten aus Drittquelle'], + }, + softFactors: { + prestige: 88, + accessibility: 93, + visibilityScore: 89, + passerbyFrequency: 'HIGH', + talentAccess: 76, + parkingSpots: 0, + publicTransportMinutes: 4, + }, + hardFacts: { floor: 0, parking: 0, hasStorefront: true, fitOut: 'FULL', isBarrierFree: true }, + floorLevel: 0, + contractDurationMonths: 60, + ancillaryCosts: 7.5, + riskLevel: RiskLevel.MEDIUM, + images: ['https://images.unsplash.com/photo-1534398079543-7ae6d016b86a?w=800&h=400&fit=crop'], + mapImageUrl: 'https://images.unsplash.com/photo-1524661135-423995f22d0b?w=600&h=300&fit=crop', + propertyNumber: 'BE-EXT-081', + description: 'Ladenfläche an der Münstergasse, Bern Innenstadt. EG, grosse Schaufensterfront, historische Lage nahe dem Berner Münster. Vollausbau gemäss Inserat.', + units: [ + { id: 'unit-081-1', propertyId: 'prop-081', floorLevel: 0, areaSqm: 350, available: true, rentPricePerSqm: 1198 }, + ], + createdAt: '2025-05-08T08:00:00Z', + updatedAt: '2025-05-08T08:00:00Z', + }, + + { + id: 'prop-082', + title: 'Ladenfläche Länggassstrasse 20', + assetType: AssetType.RETAIL, + resultType: ResultType.VERIFIED_PORTFOLIO, + location: { city: 'Bern', district: 'Länggasse', canton: 'BE', country: 'CH', coordinates: { lat: 46.954, lng: 7.432 } }, + address: { street: 'Länggassstrasse', houseNumber: '20', postalCode: '3012', city: 'Bern', country: 'CH' }, + areaSqm: 230, + rentPricePerSqm: 778, + totalRentMonthly: 178940, + availabilityDate: '2026-07-01', + availabilityStatus: AvailabilityStatus.AVAILABLE_SOON, + sourceType: 'ERP_IMPORT', + confidenceScore: 0.91, + dataQuality: { + score: 0.88, + missingCriticalFields: [], + missingOptionalFields: [], + lastVerifiedAt: '2025-05-15', + freshness: DataFreshness.FRESH, + warnings: ['Standort ausserhalb Bern Innenstadt — Near-Miss'], + }, + softFactors: { + prestige: 58, + accessibility: 82, + visibilityScore: 62, + passerbyFrequency: 'MEDIUM', + talentAccess: 68, + parkingSpots: 3, + publicTransportMinutes: 6, + }, + hardFacts: { floor: 0, parking: 3, hasStorefront: false, fitOut: 'BASIC', isBarrierFree: true }, + floorLevel: 0, + contractDurationMonths: 60, + ancillaryCosts: 5.5, + riskLevel: RiskLevel.LOW, + images: ['https://images.unsplash.com/photo-1555529669-e69e7aa0ba9a?w=800&h=400&fit=crop'], + mapImageUrl: 'https://images.unsplash.com/photo-1524661135-423995f22d0b?w=600&h=300&fit=crop', + propertyNumber: 'BE-2024-082', + description: 'Ladenfläche im Quartier Länggasse, Bern. EG, kein Schaufenster, Basisausbau — Near-Miss für Innenstadt-Suche. Ruhigeres Wohnquartier ausserhalb des Zentrums. Verfügbar Juli 2026.', + units: [ + { id: 'unit-082-1', propertyId: 'prop-082', floorLevel: 0, unitLabel: 'EG Laden', areaSqm: 230, available: true, rentPricePerSqm: 778 }, + ], + importedFrom: 'SAP RE-FX', + importedAt: '2025-01-15T08:00:00Z', + lastUpdatedAt: '2025-05-15T10:00:00Z', + breakoutOption: false, + schattenmarktRelease: { enabled: false }, + organizationId: 'org-wincasa', + createdAt: '2025-05-22T10:00:00Z', + updatedAt: '2025-05-22T10:00:00Z', + }, + + { + id: 'prop-083', + title: 'Retailfläche Neuengasse 2', + assetType: AssetType.RETAIL, + resultType: ResultType.MAISON_WORK, + location: { city: 'Bern', district: 'Innenstadt', canton: 'BE', country: 'CH', coordinates: { lat: 46.948, lng: 7.447 } }, + address: { street: 'Neuengasse', houseNumber: '2', postalCode: '3011', city: 'Bern', country: 'CH' }, + areaSqm: 295, + rentPricePerSqm: 1098, + totalRentMonthly: 323910, + availabilityDate: '2026-06-01', + availabilityStatus: AvailabilityStatus.AVAILABLE_NOW, + sourceType: 'MAISON_WORK_SCRAPE', + sourceUrl: 'https://example.com/listing/prop-083', + confidenceScore: 0.78, + dataQuality: { + score: 0.74, + missingCriticalFields: [], + missingOptionalFields: ['ancillaryCosts', 'expansionPotentialSqm'], + lastVerifiedAt: '2025-05-12', + freshness: DataFreshness.STALE, + warnings: ['Mietpreis nicht final bestätigt'], + }, + softFactors: { + prestige: 82, + accessibility: 93, + visibilityScore: 85, + passerbyFrequency: 'HIGH', + talentAccess: 74, + parkingSpots: 0, + publicTransportMinutes: 4, + }, + hardFacts: { floor: 0, parking: 0, hasStorefront: true, fitOut: 'FULL', isBarrierFree: true }, + floorLevel: 0, + contractDurationMonths: 48, + ancillaryCosts: 7.0, + riskLevel: RiskLevel.MEDIUM, + images: ['https://images.unsplash.com/photo-1528698827591-e19ccd7bc23d?w=800&h=400&fit=crop'], + mapImageUrl: 'https://images.unsplash.com/photo-1524661135-423995f22d0b?w=600&h=300&fit=crop', + propertyNumber: 'BE-MW-083', + description: 'Retailfläche an der Neuengasse, Bern Marktgasse-Umfeld. EG, Schaufensterfront, hohe Passantenfrequenz. Vollausbau gemäss Maison Work Inserat.', + units: [ + { id: 'unit-083-1', propertyId: 'prop-083', floorLevel: 0, areaSqm: 295, available: true, rentPricePerSqm: 1098 }, + ], + createdAt: '2025-05-12T08:00:00Z', + updatedAt: '2025-05-12T08:00:00Z', + }, + + // ── UC-F extras: Logistics Basel ───────────────────────────────────────────── + + { + id: 'prop-084', + title: 'Lagerhalle Rheinstrasse 22', + assetType: AssetType.LOGISTICS, + resultType: ResultType.VERIFIED_PORTFOLIO, + location: { city: 'Basel', district: 'Kleinhüningen', canton: 'BS', country: 'CH', coordinates: { lat: 47.574, lng: 7.591 } }, + address: { street: 'Rheinstrasse', houseNumber: '22', postalCode: '4057', city: 'Basel', country: 'CH' }, + areaSqm: 2200, + rentPricePerSqm: 160, + totalRentMonthly: 352000, + availabilityDate: '2025-10-01', + availabilityStatus: AvailabilityStatus.AVAILABLE_SOON, + sourceType: 'ERP_IMPORT', + confidenceScore: 0.95, + dataQuality: { + score: 0.92, + missingCriticalFields: [], + missingOptionalFields: [], + lastVerifiedAt: '2025-05-15', + freshness: DataFreshness.FRESH, + warnings: [], + }, + softFactors: { + prestige: 35, + accessibility: 80, + visibilityScore: 30, + talentAccess: 45, + parkingSpots: 30, + publicTransportMinutes: 14, + }, + hardFacts: { floor: 0, parking: 30, loadingDocksCount: 4, ceilingHeightM: 12.5, isBarrierFree: true }, + floorLevel: 0, + contractDurationMonths: 120, + ancillaryCosts: 2.5, + riskLevel: RiskLevel.LOW, + images: ['https://images.unsplash.com/photo-1590239926044-4131a46e3f27?w=800&h=400&fit=crop'], + mapImageUrl: 'https://images.unsplash.com/photo-1524661135-423995f22d0b?w=600&h=300&fit=crop', + propertyNumber: 'BS-2024-084', + description: 'Moderne Logistikhalle in Basel Kleinhüningen. 2200 m², 4 Andocktore, Deckenhöhe 12,5 m, ebenerdige Zufahrt. Ideale Lage für Nordwestschweiz-Distribution, Rheinanschluss in der Nähe. Verfügbar Oktober 2025.', + units: [ + { id: 'unit-084-1', propertyId: 'prop-084', floorLevel: 0, unitLabel: 'Lagerhalle', areaSqm: 2200, available: true, rentPricePerSqm: 160 }, + ], + importedFrom: 'SAP RE-FX', + importedAt: '2025-01-15T08:00:00Z', + lastUpdatedAt: '2025-05-15T10:00:00Z', + breakoutOption: false, + schattenmarktRelease: { enabled: false }, + organizationId: 'org-wincasa', + createdAt: '2025-05-22T10:00:00Z', + updatedAt: '2025-05-22T10:00:00Z', + }, + + { + id: 'prop-085', + title: 'Lagerhalle Elsässerstrasse 44', + assetType: AssetType.LOGISTICS, + resultType: ResultType.VERIFIED_PORTFOLIO, + location: { city: 'Basel', district: 'St. Johann', canton: 'BS', country: 'CH', coordinates: { lat: 47.558, lng: 7.572 } }, + address: { street: 'Elsässerstrasse', houseNumber: '44', postalCode: '4057', city: 'Basel', country: 'CH' }, + areaSqm: 2500, + rentPricePerSqm: 152, + totalRentMonthly: 380000, + availabilityDate: '2026-01-01', + availabilityStatus: AvailabilityStatus.AVAILABLE_SOON, + sourceType: 'ERP_IMPORT', + confidenceScore: 0.94, + dataQuality: { + score: 0.91, + missingCriticalFields: [], + missingOptionalFields: [], + lastVerifiedAt: '2025-05-15', + freshness: DataFreshness.FRESH, + warnings: [], + }, + softFactors: { + prestige: 38, + accessibility: 82, + visibilityScore: 28, + talentAccess: 42, + parkingSpots: 35, + publicTransportMinutes: 12, + }, + hardFacts: { floor: 0, parking: 35, loadingDocksCount: 3, ceilingHeightM: 12.0, isBarrierFree: true }, + floorLevel: 0, + contractDurationMonths: 120, + ancillaryCosts: 2.5, + riskLevel: RiskLevel.LOW, + images: ['https://images.unsplash.com/photo-1590239926044-4131a46e3f27?w=800&h=400&fit=crop'], + mapImageUrl: 'https://images.unsplash.com/photo-1524661135-423995f22d0b?w=600&h=300&fit=crop', + propertyNumber: 'BS-2024-085', + description: 'Logistikhalle in Basel St. Johann. 2500 m², 3 Andocktore, Deckenhöhe 12 m. Gute Autobahnanbindung A2/A3. Verfügbar Januar 2026.', + units: [ + { id: 'unit-085-1', propertyId: 'prop-085', floorLevel: 0, unitLabel: 'Lagerhalle', areaSqm: 2500, available: true, rentPricePerSqm: 152 }, + ], + importedFrom: 'SAP RE-FX', + importedAt: '2025-01-15T08:00:00Z', + lastUpdatedAt: '2025-05-15T10:00:00Z', + breakoutOption: false, + schattenmarktRelease: { enabled: false }, + organizationId: 'org-wincasa', + createdAt: '2025-05-22T10:00:00Z', + updatedAt: '2025-05-22T10:00:00Z', + }, + + { + id: 'prop-086', + title: 'Lagerhalle Industriestrasse 88, Muttenz', + assetType: AssetType.LOGISTICS, + resultType: ResultType.VERIFIED_PORTFOLIO, + location: { city: 'Muttenz', district: 'Industriezone', canton: 'BL', country: 'CH', coordinates: { lat: 47.524, lng: 7.645 } }, + address: { street: 'Industriestrasse', houseNumber: '88', postalCode: '4132', city: 'Muttenz', country: 'CH' }, + areaSqm: 2800, + rentPricePerSqm: 142, + totalRentMonthly: 397600, + availabilityDate: '2026-04-01', + availabilityStatus: AvailabilityStatus.AVAILABLE_SOON, + sourceType: 'ERP_IMPORT', + confidenceScore: 0.93, + dataQuality: { + score: 0.90, + missingCriticalFields: [], + missingOptionalFields: [], + lastVerifiedAt: '2025-05-15', + freshness: DataFreshness.FRESH, + warnings: [], + }, + softFactors: { + prestige: 30, + accessibility: 75, + visibilityScore: 22, + talentAccess: 38, + parkingSpots: 40, + publicTransportMinutes: 16, + }, + hardFacts: { floor: 0, parking: 40, loadingDocksCount: 3, ceilingHeightM: 12.0, isBarrierFree: true }, + floorLevel: 0, + contractDurationMonths: 120, + ancillaryCosts: 2.2, + riskLevel: RiskLevel.LOW, + images: ['https://images.unsplash.com/photo-1590239926044-4131a46e3f27?w=800&h=400&fit=crop'], + mapImageUrl: 'https://images.unsplash.com/photo-1524661135-423995f22d0b?w=600&h=300&fit=crop', + propertyNumber: 'BL-2024-086', + description: 'Grosse Logistikhalle in der Industriezone Muttenz. 2800 m², 3 Andocktore, Deckenhöhe 12 m, direkte LKW-Zufahrt. Günstigste Option im Grossraum Basel. Verfügbar April 2026.', + units: [ + { id: 'unit-086-1', propertyId: 'prop-086', floorLevel: 0, unitLabel: 'Lagerhalle', areaSqm: 2800, available: true, rentPricePerSqm: 142 }, + ], + importedFrom: 'SAP RE-FX', + importedAt: '2025-01-15T08:00:00Z', + lastUpdatedAt: '2025-05-15T10:00:00Z', + breakoutOption: false, + schattenmarktRelease: { enabled: false }, + organizationId: 'org-wincasa', + createdAt: '2025-05-22T10:00:00Z', + updatedAt: '2025-05-22T10:00:00Z', + }, + + { + id: 'prop-087', + title: 'Lagerhalle Hauptstrasse 180, Pratteln', + assetType: AssetType.LOGISTICS, + resultType: ResultType.EXTERNAL_MARKET, + location: { city: 'Pratteln', district: 'Industriezone', canton: 'BL', country: 'CH', coordinates: { lat: 47.519, lng: 7.689 } }, + address: { street: 'Hauptstrasse', houseNumber: '180', postalCode: '4133', city: 'Pratteln', country: 'CH' }, + areaSqm: 3000, + rentPricePerSqm: 125, + totalRentMonthly: 375000, + availabilityDate: '2026-06-01', + availabilityStatus: AvailabilityStatus.AVAILABLE_NOW, + sourceType: 'IMMOSCOUT_SCRAPE', + sourceUrl: 'https://example.com/listing/prop-087', + confidenceScore: 0.68, + dataQuality: { + score: 0.64, + missingCriticalFields: ['contractDurationMonths'], + missingOptionalFields: ['ancillaryCosts'], + lastVerifiedAt: '2025-05-08', + freshness: DataFreshness.STALE, + warnings: ['Daten aus Drittquelle', 'Hallenhöhe nicht bestätigt'], + }, + softFactors: { + prestige: 28, + accessibility: 72, + visibilityScore: 20, + talentAccess: 35, + parkingSpots: 50, + publicTransportMinutes: 18, + }, + hardFacts: { floor: 0, parking: 50, loadingDocksCount: 4, ceilingHeightM: 12.0, isBarrierFree: true }, + floorLevel: 0, + contractDurationMonths: 60, + ancillaryCosts: 2.0, + riskLevel: RiskLevel.MEDIUM, + images: ['https://images.unsplash.com/photo-1590239926044-4131a46e3f27?w=800&h=400&fit=crop'], + mapImageUrl: 'https://images.unsplash.com/photo-1524661135-423995f22d0b?w=600&h=300&fit=crop', + propertyNumber: 'BL-EXT-087', + description: 'Grosse Lagerhalle in Pratteln. 3000 m², 4 Andocktore, günstigster Flächenpreis im Vergleich. Ausserhalb Basel Stadtgebiet, jedoch gute Autobahnanbindung.', + units: [ + { id: 'unit-087-1', propertyId: 'prop-087', floorLevel: 0, unitLabel: 'Lagerhalle', areaSqm: 3000, available: true, rentPricePerSqm: 125 }, + ], + createdAt: '2025-05-08T08:00:00Z', + updatedAt: '2025-05-08T08:00:00Z', + }, + + { + id: 'prop-088', + title: 'Lagerhalle Westquaistrasse 12', + assetType: AssetType.LOGISTICS, + resultType: ResultType.EXTERNAL_MARKET, + location: { city: 'Basel', district: 'Kleinhüningen', canton: 'BS', country: 'CH', coordinates: { lat: 47.574, lng: 7.591 } }, + address: { street: 'Westquaistrasse', houseNumber: '12', postalCode: '4057', city: 'Basel', country: 'CH' }, + areaSqm: 2600, + rentPricePerSqm: 155, + totalRentMonthly: 403000, + availabilityDate: '2026-06-01', + availabilityStatus: AvailabilityStatus.AVAILABLE_NOW, + sourceType: 'HOMEGATE_SCRAPE', + sourceUrl: 'https://example.com/listing/prop-088', + confidenceScore: 0.70, + dataQuality: { + score: 0.66, + missingCriticalFields: ['contractDurationMonths'], + missingOptionalFields: ['ancillaryCosts'], + lastVerifiedAt: '2025-05-10', + freshness: DataFreshness.STALE, + warnings: ['Daten aus Drittquelle', 'Anzahl Andocktore nicht bestätigt'], + }, + softFactors: { + prestige: 36, + accessibility: 82, + visibilityScore: 28, + talentAccess: 44, + parkingSpots: 38, + publicTransportMinutes: 14, + }, + hardFacts: { floor: 0, parking: 38, loadingDocksCount: 2, ceilingHeightM: 11.5, isBarrierFree: true }, + floorLevel: 0, + contractDurationMonths: 60, + ancillaryCosts: 2.5, + riskLevel: RiskLevel.MEDIUM, + images: ['https://images.unsplash.com/photo-1590239926044-4131a46e3f27?w=800&h=400&fit=crop'], + mapImageUrl: 'https://images.unsplash.com/photo-1524661135-423995f22d0b?w=600&h=300&fit=crop', + propertyNumber: 'BS-EXT-088', + description: 'Lagerhalle im Basler Hafen, Westquaistrasse. 2600 m², 2 Andocktore, direkte Hafenlage. Andocktoranzahl gering — Near-Miss für Anfragen mit 3+ Toren.', + units: [ + { id: 'unit-088-1', propertyId: 'prop-088', floorLevel: 0, unitLabel: 'Lagerhalle', areaSqm: 2600, available: true, rentPricePerSqm: 155 }, + ], + createdAt: '2025-05-10T08:00:00Z', + updatedAt: '2025-05-10T08:00:00Z', + }, + + { + id: 'prop-089', + title: 'Lagerhalle Hochbergerstrasse 60 (Signal: Auszug)', + assetType: AssetType.LOGISTICS, + resultType: ResultType.FUTURE_AVAILABILITY, + location: { city: 'Basel', district: 'Kleinhüningen', canton: 'BS', country: 'CH', coordinates: { lat: 47.574, lng: 7.591 } }, + address: { street: 'Hochbergerstrasse', houseNumber: '60', postalCode: '4057', city: 'Basel', country: 'CH' }, + areaSqm: 2400, + rentPricePerSqm: 148, + availabilityDate: '2026-07-01', + availabilityStatus: AvailabilityStatus.AVAILABLE_SOON, + sourceType: 'ERP_IMPORT', + confidenceScore: 0.82, + dataQuality: { + score: 0.78, + missingCriticalFields: [], + missingOptionalFields: ['expansionPotentialSqm'], + lastVerifiedAt: '2025-05-15', + freshness: DataFreshness.FRESH, + warnings: ['Aktuell vermietet — Pre-Market-Freigabe aktiv'], + }, + softFactors: { + prestige: 34, + accessibility: 80, + visibilityScore: 26, + talentAccess: 42, + parkingSpots: 35, + publicTransportMinutes: 14, + }, + hardFacts: { floor: 0, parking: 35, loadingDocksCount: 3, ceilingHeightM: 12.0, isBarrierFree: true }, + floorLevel: 0, + contractDurationMonths: 120, + ancillaryCosts: 2.5, + riskLevel: RiskLevel.LOW, + images: ['https://images.unsplash.com/photo-1590239926044-4131a46e3f27?w=800&h=400&fit=crop'], + mapImageUrl: 'https://images.unsplash.com/photo-1524661135-423995f22d0b?w=600&h=300&fit=crop', + propertyNumber: 'BS-2024-089', + description: 'Logistikhalle Hochbergerstrasse 60, Basel Kleinhüningen. Derzeit von Spedition AG belegt — Auszug Juli 2026 bekannt. Pre-Market-Freigabe aktiv. 2400 m², 3 Andocktore, Deckenhöhe 12 m.', + units: [ + { id: 'unit-089-1', propertyId: 'prop-089', floorLevel: 0, unitLabel: 'Lagerhalle', areaSqm: 2400, available: false, rentPricePerSqm: 148, currentTenant: 'Spedition AG', leaseEndDate: '2026-07-01', schattenmarktRelease: { enabled: true, availableFrom: '2026-07-01' } }, + ], + importedFrom: 'SAP RE-FX', + importedAt: '2025-01-15T08:00:00Z', + lastUpdatedAt: '2025-05-15T10:00:00Z', + currentTenant: 'Spedition AG', + leaseEndDate: '2026-07-01', + breakoutOption: false, + schattenmarktRelease: { enabled: true, leadTimeMonths: 4 }, + organizationId: 'org-wincasa', + createdAt: '2025-05-22T10:00:00Z', + updatedAt: '2025-05-22T10:00:00Z', + }, + + // ── UC-D extras: Office Zürich-West 800–1000m² ──────────────────────────────── + + { + id: 'prop-090', + title: 'Bürofläche Hardstrasse 235', + assetType: AssetType.OFFICE, + resultType: ResultType.VERIFIED_PORTFOLIO, + location: { city: 'Zürich', district: 'Zürich-West', canton: 'ZH', country: 'CH', coordinates: { lat: 47.388, lng: 8.510 } }, + address: { street: 'Hardstrasse', houseNumber: '235', postalCode: '8005', city: 'Zürich', country: 'CH' }, + areaSqm: 850, + rentPricePerSqm: 478, + totalRentMonthly: 406300, + availabilityDate: '2025-10-01', + availabilityStatus: AvailabilityStatus.AVAILABLE_SOON, + sourceType: 'ERP_IMPORT', + confidenceScore: 0.95, + dataQuality: { + score: 0.92, + missingCriticalFields: [], + missingOptionalFields: [], + lastVerifiedAt: '2025-05-15', + freshness: DataFreshness.FRESH, + warnings: [], + }, + softFactors: { + prestige: 62, + accessibility: 86, + visibilityScore: 52, + talentAccess: 78, + parkingSpots: 20, + publicTransportMinutes: 6, + }, + hardFacts: { floor: 2, parking: 20, fitOut: 'BASIC', ceilingHeightM: 2.8 }, + floorLevel: 2, + contractDurationMonths: 84, + ancillaryCosts: 4.5, + riskLevel: RiskLevel.LOW, + images: ['https://images.unsplash.com/photo-1497366216548-37526070297c?w=800&h=400&fit=crop'], + mapImageUrl: 'https://images.unsplash.com/photo-1524661135-423995f22d0b?w=600&h=300&fit=crop', + propertyNumber: 'ZH-2023-090', + description: 'Grosszügige Bürofläche an der Hardstrasse, Zürich-West. 850 m² im 2. OG, Basisausbau, 20 Parkplätze. Ideales Umfeld für mittelgrosse Unternehmen. Verfügbar Oktober 2025.', + units: [ + { id: 'unit-090-1', propertyId: 'prop-090', floorLevel: 2, unitLabel: '2.OG', areaSqm: 850, available: true, rentPricePerSqm: 478 }, + ], + importedFrom: 'SAP RE-FX', + importedAt: '2025-01-15T08:00:00Z', + lastUpdatedAt: '2025-05-15T10:00:00Z', + breakoutOption: false, + schattenmarktRelease: { enabled: false }, + organizationId: 'org-wincasa', + createdAt: '2025-05-22T10:00:00Z', + updatedAt: '2025-05-22T10:00:00Z', + }, + + { + id: 'prop-091', + title: 'Bürofläche Pfingstweidstrasse 50', + assetType: AssetType.OFFICE, + resultType: ResultType.VERIFIED_PORTFOLIO, + location: { city: 'Zürich', district: 'Zürich-West', canton: 'ZH', country: 'CH', coordinates: { lat: 47.388, lng: 8.510 } }, + address: { street: 'Pfingstweidstrasse', houseNumber: '50', postalCode: '8005', city: 'Zürich', country: 'CH' }, + areaSqm: 920, + rentPricePerSqm: 508, + totalRentMonthly: 467360, + availabilityDate: '2026-01-01', + availabilityStatus: AvailabilityStatus.AVAILABLE_SOON, + sourceType: 'ERP_IMPORT', + confidenceScore: 0.94, + dataQuality: { + score: 0.92, + missingCriticalFields: [], + missingOptionalFields: [], + lastVerifiedAt: '2025-05-15', + freshness: DataFreshness.FRESH, + warnings: [], + }, + softFactors: { + prestige: 65, + accessibility: 88, + visibilityScore: 55, + talentAccess: 80, + parkingSpots: 15, + publicTransportMinutes: 5, + }, + hardFacts: { floor: 3, parking: 15, fitOut: 'FULL', ceilingHeightM: 3.2 }, + floorLevel: 3, + contractDurationMonths: 84, + ancillaryCosts: 5.0, + riskLevel: RiskLevel.LOW, + images: ['https://images.unsplash.com/photo-1613545325278-f24b0cae1224?w=800&h=400&fit=crop'], + mapImageUrl: 'https://images.unsplash.com/photo-1524661135-423995f22d0b?w=600&h=300&fit=crop', + propertyNumber: 'ZH-2023-091', + description: 'Vollausgebaute Bürofläche im 3. OG, Pfingstweidstrasse 50, Zürich-West. 920 m², FULL-Ausbau mit hohen Decken (3.2 m), 15 Parkplätze. Verfügbar Januar 2026.', + units: [ + { id: 'unit-091-1', propertyId: 'prop-091', floorLevel: 3, unitLabel: '3.OG', areaSqm: 920, available: true, rentPricePerSqm: 508 }, + ], + importedFrom: 'SAP RE-FX', + importedAt: '2025-01-15T08:00:00Z', + lastUpdatedAt: '2025-05-15T10:00:00Z', + breakoutOption: false, + schattenmarktRelease: { enabled: false }, + organizationId: 'org-wincasa', + createdAt: '2025-05-22T10:00:00Z', + updatedAt: '2025-05-22T10:00:00Z', + }, + + { + id: 'prop-092', + title: 'Bürofläche Hohlstrasse 534, Altstetten', + assetType: AssetType.OFFICE, + resultType: ResultType.EXTERNAL_MARKET, + location: { city: 'Zürich', district: 'Altstetten', canton: 'ZH', country: 'CH', coordinates: { lat: 47.385, lng: 8.488 } }, + address: { street: 'Hohlstrasse', houseNumber: '534', postalCode: '8048', city: 'Zürich', country: 'CH' }, + areaSqm: 900, + rentPricePerSqm: 438, + totalRentMonthly: 394200, + availabilityDate: '2026-06-01', + availabilityStatus: AvailabilityStatus.AVAILABLE_NOW, + sourceType: 'IMMOSCOUT_SCRAPE', + sourceUrl: 'https://example.com/listing/prop-092', + confidenceScore: 0.70, + dataQuality: { + score: 0.65, + missingCriticalFields: ['contractDurationMonths'], + missingOptionalFields: ['ancillaryCosts'], + lastVerifiedAt: '2025-05-08', + freshness: DataFreshness.STALE, + warnings: ['Daten aus Drittquelle'], + }, + softFactors: { + prestige: 50, + accessibility: 80, + visibilityScore: 44, + talentAccess: 65, + parkingSpots: 15, + publicTransportMinutes: 10, + }, + hardFacts: { floor: 1, parking: 15, fitOut: 'BASIC', ceilingHeightM: 2.8 }, + floorLevel: 1, + contractDurationMonths: 60, + ancillaryCosts: 4.0, + riskLevel: RiskLevel.MEDIUM, + images: ['https://images.unsplash.com/photo-1522071820081-009f0129c71c?w=800&h=400&fit=crop'], + mapImageUrl: 'https://images.unsplash.com/photo-1524661135-423995f22d0b?w=600&h=300&fit=crop', + propertyNumber: 'ZH-EXT-092', + description: 'Bürofläche im 1. OG, Hohlstrasse Altstetten. 900 m², Basisausbau, 15 Parkplätze. Günstigere Lage ausserhalb Zürich-West — Kompromissoption.', + units: [ + { id: 'unit-092-1', propertyId: 'prop-092', floorLevel: 1, areaSqm: 900, available: true, rentPricePerSqm: 438 }, + ], + createdAt: '2025-05-08T08:00:00Z', + updatedAt: '2025-05-08T08:00:00Z', + }, + + { + id: 'prop-093', + title: 'Bürofläche Technopark Zürich-West', + assetType: AssetType.OFFICE, + resultType: ResultType.EXTERNAL_MARKET, + location: { city: 'Zürich', district: 'Zürich-West', canton: 'ZH', country: 'CH', coordinates: { lat: 47.388, lng: 8.510 } }, + address: { street: 'Technoparkstrasse', houseNumber: '2', postalCode: '8005', city: 'Zürich', country: 'CH' }, + areaSqm: 960, + rentPricePerSqm: 522, + totalRentMonthly: 501120, + availabilityDate: '2026-06-01', + availabilityStatus: AvailabilityStatus.AVAILABLE_NOW, + sourceType: 'HOMEGATE_SCRAPE', + sourceUrl: 'https://example.com/listing/prop-093', + confidenceScore: 0.72, + dataQuality: { + score: 0.68, + missingCriticalFields: ['contractDurationMonths'], + missingOptionalFields: ['ancillaryCosts'], + lastVerifiedAt: '2025-05-10', + freshness: DataFreshness.STALE, + warnings: ['Daten aus Drittquelle'], + }, + softFactors: { + prestige: 68, + accessibility: 86, + visibilityScore: 58, + talentAccess: 84, + parkingSpots: 12, + publicTransportMinutes: 5, + }, + hardFacts: { floor: 4, parking: 12, fitOut: 'FULL', ceilingHeightM: 3.2 }, + floorLevel: 4, + contractDurationMonths: 60, + ancillaryCosts: 5.0, + riskLevel: RiskLevel.MEDIUM, + images: ['https://images.unsplash.com/photo-1613545325278-f24b0cae1224?w=800&h=400&fit=crop'], + mapImageUrl: 'https://images.unsplash.com/photo-1524661135-423995f22d0b?w=600&h=300&fit=crop', + propertyNumber: 'ZH-EXT-093', + description: 'Vollausgebaute Bürofläche im Technopark, Escher-Wyss-Areal, Zürich-West. 960 m², 4. OG, FULL-Ausbau, guter Talent-Zugang.', + units: [ + { id: 'unit-093-1', propertyId: 'prop-093', floorLevel: 4, areaSqm: 960, available: true, rentPricePerSqm: 522 }, + ], + createdAt: '2025-05-10T08:00:00Z', + updatedAt: '2025-05-10T08:00:00Z', + }, + + { + id: 'prop-094', + title: 'Bürofläche Werkstrasse 22', + assetType: AssetType.OFFICE, + resultType: ResultType.VERIFIED_PORTFOLIO, + location: { city: 'Zürich', district: 'Kreis 5', canton: 'ZH', country: 'CH', coordinates: { lat: 47.382, lng: 8.521 } }, + address: { street: 'Werkstrasse', houseNumber: '22', postalCode: '8005', city: 'Zürich', country: 'CH' }, + areaSqm: 880, + rentPricePerSqm: 492, + totalRentMonthly: 433056, + availabilityDate: '2026-04-01', + availabilityStatus: AvailabilityStatus.AVAILABLE_SOON, + sourceType: 'ERP_IMPORT', + confidenceScore: 0.93, + dataQuality: { + score: 0.91, + missingCriticalFields: [], + missingOptionalFields: [], + lastVerifiedAt: '2025-05-15', + freshness: DataFreshness.FRESH, + warnings: [], + }, + softFactors: { + prestige: 60, + accessibility: 86, + visibilityScore: 50, + talentAccess: 76, + parkingSpots: 18, + publicTransportMinutes: 6, + }, + hardFacts: { floor: 2, parking: 18, fitOut: 'BASIC', ceilingHeightM: 2.8 }, + floorLevel: 2, + contractDurationMonths: 84, + ancillaryCosts: 4.5, + riskLevel: RiskLevel.LOW, + images: ['https://images.unsplash.com/photo-1497366216548-37526070297c?w=800&h=400&fit=crop'], + mapImageUrl: 'https://images.unsplash.com/photo-1524661135-423995f22d0b?w=600&h=300&fit=crop', + propertyNumber: 'ZH-2023-094', + description: 'Bürofläche im 2. OG, Werkstrasse 22, Zürich Kreis 5. 880 m², Basisausbau, 18 Parkplätze. Solider Industrie-Standort mit guter ÖV-Anbindung. Verfügbar April 2026.', + units: [ + { id: 'unit-094-1', propertyId: 'prop-094', floorLevel: 2, unitLabel: '2.OG', areaSqm: 880, available: true, rentPricePerSqm: 492 }, + ], + importedFrom: 'SAP RE-FX', + importedAt: '2025-01-15T08:00:00Z', + lastUpdatedAt: '2025-05-15T10:00:00Z', + breakoutOption: false, + schattenmarktRelease: { enabled: false }, + organizationId: 'org-wincasa', + createdAt: '2025-05-22T10:00:00Z', + updatedAt: '2025-05-22T10:00:00Z', + }, + + // ── Misc filler ─────────────────────────────────────────────────────────────── + + { + id: 'prop-095', + title: 'Gewerbehalle Industriestrasse 30, Schlieren', + assetType: AssetType.LIGHT_INDUSTRIAL, + resultType: ResultType.VERIFIED_PORTFOLIO, + location: { city: 'Schlieren', district: 'Industriezone', canton: 'ZH', country: 'CH', coordinates: { lat: 47.398, lng: 8.449 } }, + address: { street: 'Industriestrasse', houseNumber: '30', postalCode: '8952', city: 'Schlieren', country: 'CH' }, + areaSqm: 450, + rentPricePerSqm: 182, + totalRentMonthly: 81900, + availabilityDate: '2026-01-01', + availabilityStatus: AvailabilityStatus.AVAILABLE_SOON, + sourceType: 'ERP_IMPORT', + confidenceScore: 0.92, + dataQuality: { + score: 0.89, + missingCriticalFields: [], + missingOptionalFields: [], + lastVerifiedAt: '2025-05-15', + freshness: DataFreshness.FRESH, + warnings: [], + }, + softFactors: { + prestige: 28, + accessibility: 72, + visibilityScore: 22, + talentAccess: 48, + parkingSpots: 12, + publicTransportMinutes: 12, + }, + hardFacts: { floor: 0, parking: 12, loadingDocksCount: 1, ceilingHeightM: 5.5, isBarrierFree: true }, + floorLevel: 0, + contractDurationMonths: 60, + ancillaryCosts: 2.8, + riskLevel: RiskLevel.LOW, + images: ['https://images.unsplash.com/photo-1572021335469-31706a17aaef?w=800&h=400&fit=crop'], + mapImageUrl: 'https://images.unsplash.com/photo-1524661135-423995f22d0b?w=600&h=300&fit=crop', + propertyNumber: 'ZH-2024-095', + description: 'Leichtindustrie-Halle in der Industriezone Schlieren. 450 m², 1 Andockrampe, Deckenhöhe 5.5 m, ebenerdige Zufahrt. Geeignet für Leichtfertigung, Montage, Lager. Verfügbar Januar 2026.', + units: [ + { id: 'unit-095-1', propertyId: 'prop-095', floorLevel: 0, unitLabel: 'Gewerbehalle', areaSqm: 450, available: true, rentPricePerSqm: 182 }, + ], + importedFrom: 'SAP RE-FX', + importedAt: '2025-01-15T08:00:00Z', + lastUpdatedAt: '2025-05-15T10:00:00Z', + breakoutOption: false, + schattenmarktRelease: { enabled: false }, + organizationId: 'org-wincasa', + createdAt: '2025-05-22T10:00:00Z', + updatedAt: '2025-05-22T10:00:00Z', + }, + + { + id: 'prop-096', + title: 'Produktionsfläche Bernstrasse 44, Dietikon', + assetType: AssetType.PRODUCTION, + resultType: ResultType.VERIFIED_PORTFOLIO, + location: { city: 'Dietikon', district: 'Industriezone', canton: 'ZH', country: 'CH', coordinates: { lat: 47.404, lng: 8.400 } }, + address: { street: 'Bernstrasse', houseNumber: '44', postalCode: '8953', city: 'Dietikon', country: 'CH' }, + areaSqm: 600, + rentPricePerSqm: 160, + totalRentMonthly: 96000, + availabilityDate: '2026-07-01', + availabilityStatus: AvailabilityStatus.AVAILABLE_SOON, + sourceType: 'ERP_IMPORT', + confidenceScore: 0.91, + dataQuality: { + score: 0.88, + missingCriticalFields: [], + missingOptionalFields: [], + lastVerifiedAt: '2025-05-15', + freshness: DataFreshness.FRESH, + warnings: [], + }, + softFactors: { + prestige: 25, + accessibility: 68, + visibilityScore: 18, + talentAccess: 42, + parkingSpots: 18, + publicTransportMinutes: 15, + }, + hardFacts: { floor: 0, parking: 18, loadingDocksCount: 2, ceilingHeightM: 6.0, isBarrierFree: true }, + floorLevel: 0, + contractDurationMonths: 60, + ancillaryCosts: 2.5, + riskLevel: RiskLevel.LOW, + images: ['https://images.unsplash.com/photo-1572021335469-31706a17aaef?w=800&h=400&fit=crop'], + mapImageUrl: 'https://images.unsplash.com/photo-1524661135-423995f22d0b?w=600&h=300&fit=crop', + propertyNumber: 'ZH-2024-096', + description: 'Produktionshalle in der Industriezone Dietikon. 600 m², 2 Andocktore, Deckenhöhe 6 m, Dreiphasenstrom. Günstige Miete, gute Autobahnanbindung A1. Verfügbar Juli 2026.', + units: [ + { id: 'unit-096-1', propertyId: 'prop-096', floorLevel: 0, unitLabel: 'Produktionshalle', areaSqm: 600, available: true, rentPricePerSqm: 160 }, + ], + importedFrom: 'SAP RE-FX', + importedAt: '2025-01-15T08:00:00Z', + lastUpdatedAt: '2025-05-15T10:00:00Z', + breakoutOption: false, + schattenmarktRelease: { enabled: false }, + organizationId: 'org-wincasa', + createdAt: '2025-05-22T10:00:00Z', + updatedAt: '2025-05-22T10:00:00Z', + }, + + { + id: 'prop-097', + title: 'Bürofläche Baarerstrasse 18, Zug', + assetType: AssetType.OFFICE, + resultType: ResultType.EXTERNAL_MARKET, + location: { city: 'Zug', district: 'Innenstadt', canton: 'ZG', country: 'CH', coordinates: { lat: 47.174, lng: 8.514 } }, + address: { street: 'Baarerstrasse', houseNumber: '18', postalCode: '6300', city: 'Zug', country: 'CH' }, + areaSqm: 320, + rentPricePerSqm: 418, + totalRentMonthly: 133760, + availabilityDate: '2026-06-01', + availabilityStatus: AvailabilityStatus.AVAILABLE_NOW, + sourceType: 'IMMOSCOUT_SCRAPE', + sourceUrl: 'https://example.com/listing/prop-097', + confidenceScore: 0.72, + dataQuality: { + score: 0.68, + missingCriticalFields: ['contractDurationMonths'], + missingOptionalFields: ['ancillaryCosts'], + lastVerifiedAt: '2025-05-10', + freshness: DataFreshness.STALE, + warnings: ['Daten aus Drittquelle'], + }, + softFactors: { + prestige: 90, + accessibility: 96, + visibilityScore: 82, + talentAccess: 86, + parkingSpots: 5, + publicTransportMinutes: 3, + }, + hardFacts: { floor: 3, parking: 5, hasAirConditioning: true, fitOut: 'PREMIUM', ceilingHeightM: 3.0, isBarrierFree: true }, + floorLevel: 3, + contractDurationMonths: 60, + ancillaryCosts: 8.0, + riskLevel: RiskLevel.MEDIUM, + images: ['https://images.unsplash.com/photo-1560472355-536de3962603?w=800&h=400&fit=crop'], + mapImageUrl: 'https://images.unsplash.com/photo-1524661135-423995f22d0b?w=600&h=300&fit=crop', + propertyNumber: 'ZG-EXT-097', + description: 'Premium-Bürofläche an der Baarerstrasse, Zug Innenstadt. 320 m², 3. OG, Klimaanlage. Steuerlich attraktiver Kanton, repräsentative Adresse.', + units: [ + { id: 'unit-097-1', propertyId: 'prop-097', floorLevel: 3, areaSqm: 320, available: true, rentPricePerSqm: 418 }, + ], + createdAt: '2025-05-10T08:00:00Z', + updatedAt: '2025-05-10T08:00:00Z', + }, + + { + id: 'prop-098', + title: 'Ladenlokal Marktgasse 14, Winterthur', + assetType: AssetType.RETAIL, + resultType: ResultType.MAISON_WORK, + location: { city: 'Winterthur', district: 'Innenstadt', canton: 'ZH', country: 'CH', coordinates: { lat: 47.497, lng: 8.724 } }, + address: { street: 'Marktgasse', houseNumber: '14', postalCode: '8400', city: 'Winterthur', country: 'CH' }, + areaSqm: 200, + rentPricePerSqm: 518, + totalRentMonthly: 103600, + availabilityDate: '2026-06-01', + availabilityStatus: AvailabilityStatus.AVAILABLE_NOW, + sourceType: 'MAISON_WORK_SCRAPE', + sourceUrl: 'https://example.com/listing/prop-098', + confidenceScore: 0.78, + dataQuality: { + score: 0.73, + missingCriticalFields: [], + missingOptionalFields: ['ancillaryCosts', 'expansionPotentialSqm'], + lastVerifiedAt: '2025-05-12', + freshness: DataFreshness.STALE, + warnings: ['Mietpreis nicht final bestätigt'], + }, + softFactors: { + prestige: 55, + accessibility: 88, + visibilityScore: 65, + passerbyFrequency: 'MEDIUM', + talentAccess: 62, + parkingSpots: 3, + publicTransportMinutes: 4, + }, + hardFacts: { floor: 0, parking: 3, hasStorefront: true, fitOut: 'FULL', isBarrierFree: true }, + floorLevel: 0, + contractDurationMonths: 48, + ancillaryCosts: 6.0, + riskLevel: RiskLevel.MEDIUM, + images: ['https://images.unsplash.com/photo-1441986300917-64674bd600d8?w=800&h=400&fit=crop'], + mapImageUrl: 'https://images.unsplash.com/photo-1524661135-423995f22d0b?w=600&h=300&fit=crop', + propertyNumber: 'WIN-MW-098', + description: 'Ladenlokal an der Marktgasse, Winterthur Innenstadt. EG, Schaufensterfront, vollständig ausgebaut. Mittlere Passantenfrequenz, gute ÖV-Anbindung.', + units: [ + { id: 'unit-098-1', propertyId: 'prop-098', floorLevel: 0, areaSqm: 200, available: true, rentPricePerSqm: 518 }, + ], + createdAt: '2025-05-12T08:00:00Z', + updatedAt: '2025-05-12T08:00:00Z', + }, + + { + id: 'prop-099', + title: 'Bürofläche Zweierstrasse 50', + assetType: AssetType.OFFICE, + resultType: ResultType.VERIFIED_PORTFOLIO, + location: { city: 'Zürich', district: 'Kreis 3', canton: 'ZH', country: 'CH', coordinates: { lat: 47.372, lng: 8.521 } }, + address: { street: 'Zweierstrasse', houseNumber: '50', postalCode: '8003', city: 'Zürich', country: 'CH' }, + areaSqm: 165, + rentPricePerSqm: 312, + totalRentMonthly: 51480, + availabilityDate: '2026-07-01', + availabilityStatus: AvailabilityStatus.AVAILABLE_SOON, + sourceType: 'ERP_IMPORT', + confidenceScore: 0.91, + dataQuality: { + score: 0.88, + missingCriticalFields: [], + missingOptionalFields: [], + lastVerifiedAt: '2025-05-15', + freshness: DataFreshness.FRESH, + warnings: [], + }, + softFactors: { + prestige: 55, + accessibility: 82, + visibilityScore: 45, + talentAccess: 68, + parkingSpots: 2, + publicTransportMinutes: 6, + }, + hardFacts: { floor: 2, parking: 2, fitOut: 'BASIC', ceilingHeightM: 2.8 }, + floorLevel: 2, + contractDurationMonths: 60, + ancillaryCosts: 3.8, + riskLevel: RiskLevel.LOW, + images: ['https://images.unsplash.com/photo-1524758631624-e2822e304c36?w=800&h=400&fit=crop'], + mapImageUrl: 'https://images.unsplash.com/photo-1524661135-423995f22d0b?w=600&h=300&fit=crop', + propertyNumber: 'ZH-2024-099', + description: 'Bürofläche im 2. OG, Zweierstrasse 50, Zürich Kreis 3. 165 m², Basisausbau, 2 Parkplätze. Ruhige Lage mit guter ÖV-Anbindung, Tramhaltestelle 3 Min. Verfügbar Juli 2026.', + units: [ + { id: 'unit-099-1', propertyId: 'prop-099', floorLevel: 2, unitLabel: '2.OG', areaSqm: 165, available: true, rentPricePerSqm: 312 }, + ], + importedFrom: 'SAP RE-FX', + importedAt: '2025-01-15T08:00:00Z', + lastUpdatedAt: '2025-05-15T10:00:00Z', + breakoutOption: false, + schattenmarktRelease: { enabled: false }, + organizationId: 'org-wincasa', + createdAt: '2025-05-22T10:00:00Z', + updatedAt: '2025-05-22T10:00:00Z', + }, + ] diff --git a/src/pages/demand/AISearch.tsx b/src/pages/demand/AISearch.tsx index 0de7f3f..d3e18d9 100644 --- a/src/pages/demand/AISearch.tsx +++ b/src/pages/demand/AISearch.tsx @@ -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, } diff --git a/src/pages/demand/Compare.tsx b/src/pages/demand/Compare.tsx index 0e13946..dd42f97 100644 --- a/src/pages/demand/Compare.tsx +++ b/src/pages/demand/Compare.tsx @@ -66,6 +66,11 @@ const CRITERION_ALIASES: Record = { 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'], } diff --git a/src/pages/demand/Results.tsx b/src/pages/demand/Results.tsx index 85b3098..a5a0232 100644 --- a/src/pages/demand/Results.tsx +++ b/src/pages/demand/Results.tsx @@ -39,7 +39,7 @@ export default function Results() { const [filterSource, setFilterSource] = useState('ALL') const [sortBy, setSortBy] = useState('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() { Standort: {activeNeed.preferredLocations.join(', ')} - {activeNeed.budgetRange && ( + {activeNeed.budgetRange && activeNeed.budgetRange.maxPerSqm > 0 && ( Budget: max. CHF {activeNeed.budgetRange.maxPerSqm}/m² )} - {activeNeed.timing && ( + {activeNeed.timing?.earliestMoveIn && !isNaN(new Date(activeNeed.timing.earliestMoveIn).getTime()) && ( Bezug ab: {new Date(activeNeed.timing.earliestMoveIn).toLocaleDateString('de-CH', { month: 'long', year: 'numeric' })} @@ -177,7 +179,7 @@ export default function Results() { onShowFutureAvailabilityChange={setShowFutureAvailability} showOwnProperties={showOwnProperties} onShowOwnPropertiesChange={setShowOwnProperties} - isPropertyManager={currentUser?.role === 'PROPERTY_MANAGER'} + isPropertyManager={isStaff} /> {isLoading ? ( diff --git a/src/provider/MockupNeedProvider.ts b/src/provider/MockupNeedProvider.ts index 3e333b2..09f4342 100644 --- a/src/provider/MockupNeedProvider.ts +++ b/src/provider/MockupNeedProvider.ts @@ -52,6 +52,7 @@ function buildMatch( positiveFactors: output.positiveFactors, negativeFactors: output.negativeFactors, allFactors: [...output.allHardFactors, ...output.allSoftFactors], + mustHaveEvaluation: output.mustHaveEvaluation, tradeoffs: output.tradeOffs ?? [], explainabilitySummary: isGoodLoc ? `${prop.location.city} trifft den Standortwunsch. Kernkriterien sind weitgehend erfüllt.` diff --git a/src/services/aiService.ts b/src/services/aiService.ts index a5f8f6d..a777cbd 100644 --- a/src/services/aiService.ts +++ b/src/services/aiService.ts @@ -139,11 +139,15 @@ export interface AIServiceProvider { function mockParseNeed(input: string): ParseNeedResult { const lower = input.toLowerCase() - // Asset type + // Asset type — RETAIL before LOGISTICS to avoid false match on "Nebenräume (Lager)" const assetType: AssetType | undefined = - lower.includes('büro') || lower.includes('office') ? 'OFFICE' - : lower.includes('logistik') || lower.includes('lager') ? 'LOGISTICS' - : lower.includes('retail') || lower.includes('laden') || lower.includes('shop') ? 'RETAIL' + lower.includes('retail') || lower.includes('laden') || lower.includes('shop') + || lower.includes('verkaufslokal') || lower.includes('ladenlokal') || lower.includes('ladenfläche') + || lower.includes('verkaufsfläche') || lower.includes('schaufenster') ? 'RETAIL' + : lower.includes('büro') || lower.includes('office') || lower.includes('sitzungszimmer') || lower.includes('arbeitsplätze') ? 'OFFICE' + : lower.includes('logistik') || lower.includes('lagerhalle') || lower.includes('lagerraum') + || (lower.includes('lager') && (lower.includes('logistik') || lower.includes('rampe') || lower.includes('palette') || lower.includes('lkw'))) ? 'LOGISTICS' + : lower.includes('lager') ? 'LOGISTICS' : lower.includes('produktion') || lower.includes('gewerbe') || lower.includes('industrie') ? 'PRODUCTION' : lower.includes('gastro') || lower.includes('restaurant') ? 'GASTRO' : undefined @@ -162,18 +166,77 @@ function mockParseNeed(input: string): ParseNeedResult { areaConfidence = 0.70 } - // Locations + // Locations — use word boundaries to avoid false matches like "Umzugsfirma" → "Zug" const CITIES: [string, string][] = [ ['zürich', 'Zürich'], ['basel', 'Basel'], ['bern', 'Bern'], ['genf', 'Genf'], ['lausanne', 'Lausanne'], ['winterthur', 'Winterthur'], ['zug', 'Zug'], ['luzern', 'Luzern'], ['st. gallen', 'St. Gallen'], ['lugano', 'Lugano'], ['biel', 'Biel'], ['schaffhausen', 'Schaffhausen'], + ['wädenswil', 'Wädenswil'], ['thalwil', 'Thalwil'], ['uster', 'Uster'], + ['horgen', 'Horgen'], ['küsnacht', 'Küsnacht'], ['baar', 'Baar'], ] - const preferredLocations = CITIES.filter(([k]) => lower.includes(k)).map(([, v]) => v) + const preferredLocations = CITIES.filter(([k]) => { + if (k.includes(' ') || k.includes('.')) return lower.includes(k) + // Word boundary: not preceded/followed by a letter (including German umlauts) + const re = new RegExp(`(? v) + + // Infer Zürich when Zürich-specific districts or landmarks are mentioned + const ZURICH_SIGNALS = [ + 'seefeld', 'bellevue', 'paradeplatz', 'bahnhofstrasse', 'zürich-west', 'zürich west', + 'oerlikon', 'altstetten', 'kreis 1', 'kreis 2', 'kreis 3', 'kreis 4', 'kreis 5', + 'kreis 6', 'kreis 7', 'kreis 8', 'langstrasse', 'hardbrücke', 'freilager', + 'europaallee', 'zürich nord', 'zürich süd', + ] + if (!preferredLocations.includes('Zürich') && ZURICH_SIGNALS.some(s => lower.includes(s))) { + preferredLocations.push('Zürich') + } + + // Contextual Zürich inference: only truly Zürich-specific place/brand names + const ZURICH_ONLY_SIGNALS = [ + 'industrie groove', 'industrie-groove', + 'pfingstweidstrasse', 'hardbrücke', 'freilager', 'europaallee', + 'zürich-west', 'zürich west', 'zürich nord', 'zürich süd', + 'hürlimann', 'viadukt', 'schiffbau', + ] + if (preferredLocations.length === 0 && ZURICH_ONLY_SIGNALS.some(s => lower.includes(s))) { + preferredLocations.push('Zürich') + } + + // Extract district list from patterns like "Kreis 3, 4, 5, und 8" or "Kreis 3/4/5" + const kreisListMatch = lower.match(/\bkreis\s+([\d]+(?:\s*[,\/]\s*[\d]+)*(?:\s+und\s+[\d]+)?)/) + if (kreisListMatch) { + const nums = kreisListMatch[1].match(/\d+/g) ?? [] + nums.forEach(n => { + const k = `Zürich Kreis ${n}` + if (!preferredLocations.includes(k)) preferredLocations.push(k) + }) + if (!preferredLocations.includes('Zürich')) preferredLocations.push('Zürich') + } + + // Map Zürich landmark/neighbourhood names → district-level location entries + const ZURICH_DISTRICT_MAP: [string, string][] = [ + ['seefeld', 'Zürich Seefeld'], ['bellevue', 'Zürich Seefeld'], + ['bahnhofstrasse', 'Zürich Kreis 1'], ['paradeplatz', 'Zürich Kreis 1'], + ['zürich-west', 'Zürich-West'], ['pfingstweidstrasse', 'Zürich-West'], + ['limmatstrasse', 'Zürich-West'], + ] + ZURICH_DISTRICT_MAP.forEach(([k, v]) => { + if (lower.includes(k) && !preferredLocations.includes(v)) preferredLocations.push(v) + }) + const locationConfidence = preferredLocations.length > 0 ? 0.88 : 0.15 - // Budget - const budgetPerSqmMatch = input.match(/(\d+)\s*(?:CHF)?\s*\/\s*m[²2]/i) + // Budget — strip Swiss price notation (320.--) before matching + const cleanedBudget = input.replace(/(\d)\.-+/g, '$1').replace(/(\d)\.—/g, '$1') + // "MZ 320/m²", "CHF 320/m²", "320/m²", "320.--/m²/a" + const budgetPerSqmMatch = cleanedBudget.match(/(?:mz|mietzins|max\.?|budget)?\s*(?:CHF\s*)?(\d{2,4})\s*\/\s*m[²2]/i) + // Range "250 – 280 CHF" or "250-280/m²" — require explicit currency or /m² so area ranges like "120–150m2" are not matched + const budgetRangeMatch = + cleanedBudget.match(/(\d{2,4})\s*[–\-]\s*(\d{2,4})\s*(?:CHF|Fr\.?)\b/i) ?? + cleanedBudget.match(/(?:CHF|Fr\.?)\s*(\d{2,4})\s*[–\-]\s*(\d{2,4})/i) ?? + cleanedBudget.match(/(\d{2,4})\s*[–\-]\s*(\d{2,4})\s*\/\s*m[²2]/i) const budgetMaxMatch = input.match(/(?:max\.?|bis|höchstens)\s*(?:CHF\s*)?(\d+)/i) let budgetRange: { maxPerSqm: number; currency: string } | undefined let budgetConfidence = 0.20 @@ -184,6 +247,11 @@ function mockParseNeed(input: string): ParseNeedResult { const raw = parseInt(budgetPerSqmMatch[1]) budgetRange = { maxPerSqm: raw < monthlyThreshold ? raw * 12 : raw, currency: 'CHF' } budgetConfidence = 0.92 + } else if (budgetRangeMatch) { + // Take upper value of range as max + const raw = parseInt(budgetRangeMatch[2]) + budgetRange = { maxPerSqm: raw < monthlyThreshold ? raw * 12 : raw, currency: 'CHF' } + budgetConfidence = 0.75 } else if (budgetMaxMatch) { const raw = parseInt(budgetMaxMatch[1]) budgetRange = { maxPerSqm: raw < monthlyThreshold ? raw * 12 : raw, currency: 'CHF' } @@ -191,7 +259,9 @@ function mockParseNeed(input: string): ParseNeedResult { } // Timing - const yearMatch = input.match(/(?:ab\s+)?(?:Q[1-4]\s*\/?\s*)?(\d{4})/) + const yearMatch = input.match(/(?:ab\s+)?(?:Q[1-4]\s*\/?\s*)?(20\d{2})/) + // "Q4/26" or "Q1/27" — two-digit year (collect all occurrences, use first as earliest) + const quarterMatches = [...input.matchAll(/Q([1-4])\s*[\/\-]?\s*(\d{2})\b/gi)] const soonMatch = lower.includes('sofort') || lower.includes('asap') let timing: ParsedNeedCriteria['timing'] | undefined let timingConfidence = 0.20 @@ -201,20 +271,81 @@ function mockParseNeed(input: string): ParseNeedResult { } else if (yearMatch) { timing = { earliestMoveIn: `${yearMatch[1]}-01-01`, latestMoveIn: `${yearMatch[1]}-12-31`, flexibleTiming: lower.includes('flexibel') } timingConfidence = 0.75 + } else if (quarterMatches.length > 0) { + // Use first Q as earliest, last Q as latest (6-month window minimum) + const qStartMonth = [0, 1, 4, 7, 10] // [unused, Q1, Q2, Q3, Q4] + const first = quarterMatches[0] + const last = quarterMatches[quarterMatches.length - 1] + const yr1 = 2000 + parseInt(first[2]) + const yr2 = 2000 + parseInt(last[2]) + const q1 = parseInt(first[1]) + const q2 = parseInt(last[1]) + const startM = String(qStartMonth[q1]).padStart(2, '0') + const endM = String(Math.min(12, qStartMonth[q2] + 2)).padStart(2, '0') + timing = { + earliestMoveIn: `${yr1}-${startM}-01`, + latestMoveIn: `${yr2}-${endM}-${endM === '12' ? '31' : '28'}`, + flexibleTiming: true, + } + timingConfidence = 0.70 } // Must-haves const mustHaveCriteria: string[] = [] - if (lower.includes('öv') || lower.includes('bahnhof') || lower.includes('tram')) mustHaveCriteria.push('Gute ÖV-Anbindung') - if (lower.includes('parking') || lower.includes('parkplatz') || lower.includes('tiefgarage')) mustHaveCriteria.push('Parkplätze vorhanden') + if (lower.includes('öv') || lower.includes('bahnhof') || lower.includes('tram') || lower.includes('erschlossen')) mustHaveCriteria.push('Gute ÖV-Anbindung') + if (lower.includes('parking') || lower.includes('parkplatz') || lower.includes('tiefgarage') || lower.includes('stellplatz')) mustHaveCriteria.push('Parkplätze vorhanden') if (lower.includes('klimaanlage') || lower.includes('klima')) mustHaveCriteria.push('Klimaanlage') if (lower.includes('server') || lower.includes('rechenzentr')) mustHaveCriteria.push('Serverraum / IT-Infrastruktur') if (lower.includes('barrierefrei')) mustHaveCriteria.push('Barrierefreiheit') if (lower.includes('küche') || lower.includes('kantine')) mustHaveCriteria.push('Kantine / Küche') + if (lower.includes('laderampe') || lower.includes('rampe') || lower.includes('verladetor')) mustHaveCriteria.push('Laderampe') + if (lower.includes('schaufenster') || lower.includes('shopfenster') || lower.includes('vitrine')) mustHaveCriteria.push('Schaufenster') + if (lower.includes('verpflegung') || lower.includes('restaurant') || lower.includes('lunch') || lower.includes('mittagessen') || lower.includes('takeaway')) mustHaveCriteria.push('Verpflegungsmöglichkeiten') + if (lower.includes('startup') || lower.includes('start-up') || lower.includes('community') || lower.includes('coworking') || lower.includes('junge firmen') || lower.includes('jungunternehm')) mustHaveCriteria.push('Startup-Community / innovative Nachbarn') + if (lower.includes('wachsen') || lower.includes('expansion') || lower.includes('erweiterungsoption') || lower.includes('wachstum') || lower.includes('wachstumsoption') || lower.includes('flexibilität zum wachsen')) mustHaveCriteria.push('Erweiterungsoption vorhanden') + if (lower.includes('werkstatt') || lower.includes('atelier') || lower.includes('reparatur')) mustHaveCriteria.push('Werkstatt / Atelier') + + // Structured requirement fields + const requireGroundFloor = lower.includes('erdgeschoss') || lower.includes('parterre') + || lower.includes('schaufenster') || lower.includes('ladenlokal') || undefined as boolean | undefined + const requireAirConditioning = lower.includes('klimaanlage') || lower.includes('klimatisierung') + || lower.includes('klima') || lower.includes('air conditioning') + || lower.includes('kühlung') || undefined as boolean | undefined + const requireLoadingDock = lower.includes('laderampe') || lower.includes('verladerampe') + || lower.includes('ladetor') || lower.includes('rampe') || undefined as boolean | undefined + const requireBarrierFree = lower.includes('barrierefrei') || lower.includes('rollstuhl') + || lower.includes('iv-gerecht') || lower.includes('behindertengerecht') || undefined as boolean | undefined + + const ceilingMatch = input.match(/(\d+(?:[.,]\d+)?)\s*m(?:eter)?\s*(?:deckenhöhe|hallenhöhe|lichte\s+höhe)/i) + ?? input.match(/deckenhöhe\s+(?:mind\.?\s*)?(\d+(?:[.,]\d+)?)\s*m/i) + const minCeilingHeightM = ceilingMatch ? parseFloat(ceilingMatch[1].replace(',', '.')) : undefined + + // Parking: "3-5 Parkplätze" → extract lower bound (minimum); "5 Parkplätze" → 5 + const parkingRangeMatch = input.match(/(\d+)\s*[-–]\s*\d+\s*(?:parkplätze?|stellplätze?|pp\b)/i) + const parkingSingleMatch = input.match(/(?:mind(?:estens)?\.?\s+)?(\d+)\s*(?:parkplätze?|stellplätze?|pp\b)/i) + const requiredParkingMin = parkingRangeMatch ? parseInt(parkingRangeMatch[1]) + : parkingSingleMatch ? parseInt(parkingSingleMatch[1]) : undefined + + const fitOutStr: 'BASIC' | 'FULL' | 'PREMIUM' | undefined = + lower.includes('schlüsselfertig') || lower.includes('premium') || lower.includes('hochwertig') + || lower.includes('top ausgebaut') || lower.includes('top-ausgebaut') ? 'PREMIUM' + : lower.includes('vollausbau') || lower.includes('vollständig ausgebaut') || lower.includes('ausgebaut') + || lower.includes('ready to use') || lower.includes('reddy to use') || lower.includes('bezugsfertig') ? 'FULL' + : lower.includes('basisausbau') || lower.includes('rohbau') || lower.includes('einfach') ? 'BASIC' + : undefined + + // Contract duration: "7-jähriger Vertrag", "Laufzeit 7 Jahre", standalone "7 Jahre" at sentence start + // Exclude "in X Jahren", "X Jahre im Geschäft", "X Jahre Erfahrung" etc. + const contractMatch = input.match(/(\d+)[- ]?j[aä]hrige?(?:r)?\s+(?:vertrag|mietvertrag|laufzeit)/i) + ?? input.match(/(?:vertrag|laufzeit|mietdauer).{0,20}(\d+)\s*jahre?/i) + ?? input.match(/\b(\d+)\s*jahre?\b(?!\s*(?:erfahrung|planung|rendite|im\b|alt\b|alten|altes|jung))/i) + const minContractDurationMonths = contractMatch ? parseInt(contractMatch[1]) * 12 : undefined // Soft const prestigeImportance: 'LOW' | 'MEDIUM' | 'HIGH' | undefined = - lower.includes('prestige') || lower.includes('representativ') || lower.includes('repräsentativ') ? 'HIGH' + lower.includes('prestige') || lower.includes('representativ') || lower.includes('repräsentativ') + || lower.includes('topadresse') || lower.includes('top adresse') || lower.includes('innerstädtisch') + || lower.includes('topaddresse') ? 'HIGH' : lower.includes('standard') ? 'LOW' : undefined const parkingNeed = lower.includes('parking') || lower.includes('parkplatz') @@ -305,16 +436,40 @@ function mockParseNeed(input: string): ParseNeedResult { importance: 'optional', }) + // Special notes: apartment wish (UC-A type) + lease options (UC-C type) + const notesParts: string[] = [] + const apartmentMatch = input.match(/(\d[.,]\d)\s*zimmer[- ]?wohn/i) + ?? (lower.includes('zimmer') && lower.includes('wohnung') ? [''] : null) + if (apartmentMatch) { + const sizeHint = apartmentMatch[1] ? `${apartmentMatch[1]}-Zi-Wohnung` : 'Wohnung' + const rentMatch = input.match(/(\d['.\s]?\d{3})[.,\-\s]*(?:inkl|inkl\.)/i) + ?? input.match(/(\d{4})[.,\-]\s*(?:chf|fr)?/i) + const rentHint = rentMatch ? ` max. CHF ${rentMatch[1].replace(/['\s]/g, "'")}` : '' + notesParts.push(`Wunsch: ${sizeHint} im Haus oder in der Nähe${rentHint} inkl. NK`) + } + const leaseOptionMatch = input.match(/(\d+)\s*[*×x]\s*(\d+)\s*jahre?\s*(?:echte\s*)?optione?n?/i) + if (leaseOptionMatch) { + notesParts.push(`Mietoption: ${leaseOptionMatch[1]}×${leaseOptionMatch[2]} Jahre echte Optionen gefordert`) + } + const notes = notesParts.length > 0 ? notesParts.join(' | ') : undefined + + // Semantic signal flags used for weight boosting + const hasExpansionSignal = lower.includes('wachsen') || lower.includes('wachstum') || lower.includes('expansion') || lower.includes('erweiterung') + const hasCommunitySignal = lower.includes('startup') || lower.includes('community') || lower.includes('coworking') || lower.includes('junge firmen') + const hasPrestigeSignal = prestigeImportance === 'HIGH' || lower.includes('repräsentativ') || lower.includes('charakter') || lower.includes('beeindrucken') + const hasFlexSignal = lower.includes('flexibel') || lower.includes('wachsen') || hasCommunitySignal + // Suggested weights const suggestedWeights: Record = { - area: 0.20, - location: preferredLocations.length > 0 ? 0.28 : 0.22, - budget: budgetRange ? 0.22 : 0.18, - timing: timing ? 0.15 : 0.12, - prestige: prestigeImportance === 'HIGH' ? 0.10 : 0.05, + area: 0.18, + location: preferredLocations.length > 0 ? 0.25 : 0.20, + budget: budgetRange ? 0.20 : 0.16, + timing: timing ? 0.14 : 0.10, + prestige: hasPrestigeSignal ? 0.10 : 0.04, accessibility: mustHaveCriteria.includes('Gute ÖV-Anbindung') ? 0.08 : 0.04, - expansionPotential: 0.03, - flexibility: lower.includes('flexibel') ? 0.07 : 0.03, + expansionPotential: hasExpansionSignal ? 0.08 : 0.03, + flexibility: hasFlexSignal ? 0.08 : 0.03, + talentAccess: hasCommunitySignal ? 0.06 : 0.03, } const rawSummary = `Bedarf analysiert: ${assetType ?? 'Typ unbekannt'} · ${preferredLocations.join(', ') || 'Standort unklar'} · ${areaRange ? `${areaRange.min}–${areaRange.max} m²` : 'Fläche unklar'} · ${budgetRange ? `max. CHF ${budgetRange.maxPerSqm}/m²` : 'Budget unklar'}` @@ -329,12 +484,21 @@ function mockParseNeed(input: string): ParseNeedResult { mustHaveCriteria, infrastructureRequirements: [], accessibilityRequirements: [], - prestigeImportance, - flexibilityNeed: lower.includes('flexibel') ? 'HIGH' : 'MEDIUM', - expansionPotential: lower.includes('wachstum') || lower.includes('expansion'), + prestigeImportance: hasPrestigeSignal ? 'HIGH' : prestigeImportance, + flexibilityNeed: hasFlexSignal ? 'HIGH' : 'MEDIUM', + expansionPotential: hasExpansionSignal, parkingNeed, visibilityNeed, footfallNeed, + requireGroundFloor: requireGroundFloor || undefined, + requireAirConditioning: requireAirConditioning || undefined, + requireLoadingDock: requireLoadingDock || undefined, + requireBarrierFree: requireBarrierFree || undefined, + requiredParkingMin, + requiredFitOut: fitOutStr, + minCeilingHeightM, + minContractDurationMonths, + notes, }, confidenceByField, missingFields, @@ -412,7 +576,7 @@ export const aiService = { // F008 methods async parseNeed(input: string): Promise> { - await new Promise(r => setTimeout(r, 1400)) + await new Promise(r => setTimeout(r, 300)) const data = mockParseNeed(input) return { data } },