fix: close spec gaps — delete InquiryStatusBadge, extend match reasons, full field keys
- Delete InquiryStatusBadge.tsx (dead code after Part A read/unread migration) - AdditionalPropertyMatch: add reasons[] + topUncertainty from positiveFactors/tradeoffs - AdditionalMatchCard: render green reasons and amber uncertainty below match score - ReportObjectFieldKey: add breakoutOptionDate, roomHeight, floorLoad, deliveryAccess, goodsLift, passengerLift, internet, microLocation, competitionEnvironment, infrastructure, marketSignals, negotiationHints, missingData (now 45 keys) - ReportObjectFieldSelector: regroup into 5 spec-aligned groups (Basisdaten, Miet-/Vertragsdaten, Technische Hard Facts, Soft Factors, Marktinformationen) - LatentInquiryReportPreview: add labels for all new field keys Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,31 +0,0 @@
|
|||||||
import { Chip } from '@mui/material'
|
|
||||||
import type { InquiryStatus } from '../../domain/inquiry'
|
|
||||||
|
|
||||||
const STATUS_MAP: Record<InquiryStatus, { label: string; bg: string }> = {
|
|
||||||
new: { label: 'Neu', bg: '#1e40af' },
|
|
||||||
in_progress: { label: 'In Bearbeitung', bg: '#d97706' },
|
|
||||||
answered: { label: 'Beantwortet', bg: '#1a7a4a' },
|
|
||||||
archived: { label: 'Archiviert', bg: '#64748b' },
|
|
||||||
}
|
|
||||||
|
|
||||||
interface InquiryStatusBadgeProps {
|
|
||||||
status: InquiryStatus
|
|
||||||
size?: 'small' | 'medium'
|
|
||||||
}
|
|
||||||
|
|
||||||
export function InquiryStatusBadge({ status, size = 'small' }: InquiryStatusBadgeProps) {
|
|
||||||
const cfg = STATUS_MAP[status]
|
|
||||||
return (
|
|
||||||
<Chip
|
|
||||||
label={cfg.label}
|
|
||||||
size={size}
|
|
||||||
sx={{
|
|
||||||
bgcolor: cfg.bg,
|
|
||||||
color: '#ffffff',
|
|
||||||
fontWeight: 600,
|
|
||||||
fontSize: '0.7rem',
|
|
||||||
height: size === 'small' ? 22 : 26,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -16,21 +16,42 @@ const FIELD_LABELS: Partial<Record<ReportObjectFieldKey, string>> = {
|
|||||||
availabilityDate: 'Verfügbar ab',
|
availabilityDate: 'Verfügbar ab',
|
||||||
leaseTerm: 'Mietlaufzeit',
|
leaseTerm: 'Mietlaufzeit',
|
||||||
breakoutOption: 'Breakout-Option',
|
breakoutOption: 'Breakout-Option',
|
||||||
|
breakoutOptionDate: 'Breakout Zeitpunkt',
|
||||||
currentTenant: 'Aktueller Mieter',
|
currentTenant: 'Aktueller Mieter',
|
||||||
propertyNumber: 'Objektnummer',
|
propertyNumber: 'Objektnummer',
|
||||||
assetType: 'Objekttyp',
|
assetType: 'Asset Type',
|
||||||
floor: 'Stockwerk',
|
floor: 'Stockwerk',
|
||||||
parking: 'Parkplätze',
|
parking: 'Parkplätze',
|
||||||
ceilingHeightM: 'Deckenhöhe',
|
fitOut: 'Ausbaugrad',
|
||||||
loadingDocksCount: 'Laderampen',
|
isBarrierFree: 'Barrierefrei',
|
||||||
|
ceilingHeightM: 'Raumhöhe',
|
||||||
|
floorLoad: 'Bodenlast',
|
||||||
|
loadingDocksCount: 'Anlieferung',
|
||||||
|
goodsLift: 'Warenaufzug',
|
||||||
|
passengerLift: 'Personenaufzug',
|
||||||
powerSupplyKva: 'Stromanschluss',
|
powerSupplyKva: 'Stromanschluss',
|
||||||
hasServerRoom: 'Serverraum',
|
hasServerRoom: 'Serverraum',
|
||||||
isBarrierFree: 'Barrierefrei',
|
internet: 'Internet',
|
||||||
fitOut: 'Ausbaustandard',
|
deliveryAccess: 'Zufahrt',
|
||||||
publicTransportScore: 'ÖV-Score',
|
publicTransportScore: 'ÖV-Anbindung',
|
||||||
prestigeScore: 'Prestige',
|
prestigeScore: 'Prestige',
|
||||||
visibilityScore: 'Sichtbarkeit',
|
visibilityScore: 'Sichtbarkeit',
|
||||||
|
footfallScore: 'Passantenfrequenz',
|
||||||
|
commuterAccessScore: 'Pendlererreichbarkeit',
|
||||||
|
talentAccessScore: 'Talent Access',
|
||||||
|
esgScore: 'ESG',
|
||||||
|
flexibilityScore: 'Flexibilität',
|
||||||
|
expansionPotentialScore: 'Expansionspotenzial',
|
||||||
|
taxEnvironmentScore: 'Steuerumfeld',
|
||||||
|
microLocation: 'Mikrostandort',
|
||||||
|
competitionEnvironment: 'Konkurrenzumfeld',
|
||||||
|
infrastructure: 'Infrastruktur',
|
||||||
|
marketSignals: 'Marktsignale',
|
||||||
|
negotiationHints: 'Verhandlungshinweise',
|
||||||
|
missingData: 'Datenlücken',
|
||||||
|
dataQuality: 'Datenqualität',
|
||||||
description: 'Beschreibung',
|
description: 'Beschreibung',
|
||||||
|
units: 'Stockwerkstruktur',
|
||||||
}
|
}
|
||||||
|
|
||||||
function getFieldValue(property: Property, key: ReportObjectFieldKey): string | null {
|
function getFieldValue(property: Property, key: ReportObjectFieldKey): string | null {
|
||||||
|
|||||||
@@ -189,9 +189,23 @@ function AdditionalMatchCard({ match }: { match: AdditionalPropertyMatch }) {
|
|||||||
{match.matchScore}%
|
{match.matchScore}%
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
<Typography variant="caption" sx={{ color: '#64748b', fontSize: '0.7rem', display: 'block' }}>
|
<Typography variant="caption" sx={{ color: '#64748b', fontSize: '0.7rem', display: 'block', mb: 0.5 }}>
|
||||||
{match.location} · {match.areaSqm.toLocaleString('de-CH')} m²
|
{match.location} · {match.areaSqm.toLocaleString('de-CH')} m² · CHF {match.rentPricePerSqm}/m²
|
||||||
</Typography>
|
</Typography>
|
||||||
|
{match.reasons.length > 0 && (
|
||||||
|
<Box sx={{ mb: 0.5 }}>
|
||||||
|
{match.reasons.map((r, i) => (
|
||||||
|
<Typography key={i} variant="caption" sx={{ color: '#15803d', fontSize: '0.68rem', display: 'block', lineHeight: 1.4 }}>
|
||||||
|
+ {r}
|
||||||
|
</Typography>
|
||||||
|
))}
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
{match.topUncertainty && (
|
||||||
|
<Typography variant="caption" sx={{ color: '#92400e', fontSize: '0.68rem', display: 'block', lineHeight: 1.4, mb: 0.25 }}>
|
||||||
|
⚠ {match.topUncertainty}
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
<Button
|
<Button
|
||||||
size="small"
|
size="small"
|
||||||
endIcon={<ArrowRight size={11} />}
|
endIcon={<ArrowRight size={11} />}
|
||||||
|
|||||||
@@ -15,58 +15,69 @@ type FieldGroup = {
|
|||||||
|
|
||||||
const FIELD_GROUPS: FieldGroup[] = [
|
const FIELD_GROUPS: FieldGroup[] = [
|
||||||
{
|
{
|
||||||
label: 'Grunddaten',
|
label: 'Basisdaten',
|
||||||
fields: [
|
fields: [
|
||||||
{ key: 'areaSqm', label: 'Fläche (m²)' },
|
{ key: 'areaSqm', label: 'Fläche' },
|
||||||
{ key: 'rentPricePerSqm', label: 'Mietpreis (CHF/m²/Jahr)' },
|
{ key: 'rentPricePerSqm', label: 'Mietpreis' },
|
||||||
{ key: 'availabilityDate', label: 'Verfügbar ab' },
|
{ key: 'availabilityDate', label: 'Verfügbar ab' },
|
||||||
|
{ key: 'propertyNumber', label: 'Objektnummer' },
|
||||||
|
{ key: 'assetType', label: 'Asset Type' },
|
||||||
|
{ key: 'description', label: 'Beschreibung' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Miet-/Vertragsdaten',
|
||||||
|
fields: [
|
||||||
{ key: 'leaseTerm', label: 'Mietlaufzeit' },
|
{ key: 'leaseTerm', label: 'Mietlaufzeit' },
|
||||||
{ key: 'breakoutOption', label: 'Breakout-Option' },
|
{ key: 'breakoutOption', label: 'Breakout-Option' },
|
||||||
|
{ key: 'breakoutOptionDate', label: 'Breakout-Option Zeitpunkt' },
|
||||||
{ key: 'currentTenant', label: 'Aktueller Mieter' },
|
{ key: 'currentTenant', label: 'Aktueller Mieter' },
|
||||||
{ key: 'propertyNumber', label: 'Objektnummer' },
|
|
||||||
{ key: 'assetType', label: 'Objekttyp' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Technische Merkmale',
|
|
||||||
fields: [
|
|
||||||
{ key: 'floor', label: 'Stockwerk' },
|
{ key: 'floor', label: 'Stockwerk' },
|
||||||
{ key: 'parking', label: 'Parkplätze' },
|
{ key: 'parking', label: 'Parkplätze' },
|
||||||
{ key: 'ceilingHeightM', label: 'Deckenhöhe (m)' },
|
{ key: 'fitOut', label: 'Ausbaugrad' },
|
||||||
{ key: 'loadingDocksCount', label: 'Laderampen' },
|
|
||||||
{ key: 'powerSupplyKva', label: 'Stromanschluss (kVA)' },
|
|
||||||
{ key: 'hasServerRoom', label: 'Serverraum' },
|
|
||||||
{ key: 'isBarrierFree', label: 'Barrierefrei' },
|
{ key: 'isBarrierFree', label: 'Barrierefrei' },
|
||||||
{ key: 'fitOut', label: 'Ausbaustandard' },
|
|
||||||
{ key: 'publicTransportScore', label: 'ÖV-Score' },
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Weiche Faktoren',
|
label: 'Technische Hard Facts',
|
||||||
|
fields: [
|
||||||
|
{ key: 'ceilingHeightM', label: 'Raumhöhe' },
|
||||||
|
{ key: 'floorLoad', label: 'Bodenlast' },
|
||||||
|
{ key: 'loadingDocksCount', label: 'Anlieferung' },
|
||||||
|
{ key: 'goodsLift', label: 'Warenaufzug' },
|
||||||
|
{ key: 'passengerLift', label: 'Personenaufzug' },
|
||||||
|
{ key: 'powerSupplyKva', label: 'Stromanschluss (kVA)' },
|
||||||
|
{ key: 'hasServerRoom', label: 'Serverraum' },
|
||||||
|
{ key: 'internet', label: 'Internet / Konnektivität' },
|
||||||
|
{ key: 'deliveryAccess', label: 'Zufahrt / Logistik' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Soft Factors',
|
||||||
fields: [
|
fields: [
|
||||||
{ key: 'prestigeScore', label: 'Prestige' },
|
{ key: 'prestigeScore', label: 'Prestige' },
|
||||||
{ key: 'visibilityScore', label: 'Sichtbarkeit' },
|
{ key: 'visibilityScore', label: 'Sichtbarkeit' },
|
||||||
{ key: 'footfallScore', label: 'Passantenfrequenz' },
|
{ key: 'footfallScore', label: 'Passantenfrequenz' },
|
||||||
{ key: 'commuterAccessScore', label: 'Pendleranbindung' },
|
{ key: 'commuterAccessScore', label: 'Pendlererreichbarkeit' },
|
||||||
{ key: 'talentAccessScore', label: 'Talentpool' },
|
{ key: 'talentAccessScore', label: 'Talent Access' },
|
||||||
{ key: 'esgScore', label: 'ESG-Bewertung' },
|
{ key: 'esgScore', label: 'ESG / Nachhaltigkeit' },
|
||||||
{ key: 'flexibilityScore', label: 'Flexibilität' },
|
{ key: 'flexibilityScore', label: 'Flexibilität' },
|
||||||
{ key: 'expansionPotentialScore', label: 'Expansionspotenzial' },
|
{ key: 'expansionPotentialScore', label: 'Expansionsmöglichkeit' },
|
||||||
{ key: 'taxEnvironmentScore', label: 'Steuerumgebung' },
|
{ key: 'taxEnvironmentScore', label: 'Steuerumfeld' },
|
||||||
|
{ key: 'publicTransportScore', label: 'ÖV-Anbindung' },
|
||||||
|
{ key: 'microLocation', label: 'Mikrostandort' },
|
||||||
|
{ key: 'competitionEnvironment', label: 'Konkurrenzumfeld' },
|
||||||
|
{ key: 'infrastructure', label: 'Infrastruktur' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Beschreibung & Medien',
|
label: 'Marktinformationen',
|
||||||
fields: [
|
|
||||||
{ key: 'description', label: 'Beschreibung' },
|
|
||||||
{ key: 'units', label: 'Stockwerkstruktur' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Datenqualität',
|
|
||||||
fields: [
|
fields: [
|
||||||
|
{ key: 'marketSignals', label: 'Marktsignale' },
|
||||||
|
{ key: 'negotiationHints', label: 'Verhandlungshinweise' },
|
||||||
|
{ key: 'missingData', label: 'Datenlücken / offene Punkte' },
|
||||||
{ key: 'dataQuality', label: 'Datenqualität' },
|
{ key: 'dataQuality', label: 'Datenqualität' },
|
||||||
{ key: 'softFactors', label: 'Weiche Faktoren (Gesamt)' },
|
{ key: 'units', label: 'Stockwerkstruktur' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ export { InquiryChat } from './InquiryChat'
|
|||||||
export { InquiryMessageBubble } from './InquiryMessageBubble'
|
export { InquiryMessageBubble } from './InquiryMessageBubble'
|
||||||
export { InquiryReplyComposer } from './InquiryReplyComposer'
|
export { InquiryReplyComposer } from './InquiryReplyComposer'
|
||||||
export { InquiryDetailPanel } from './InquiryDetailPanel'
|
export { InquiryDetailPanel } from './InquiryDetailPanel'
|
||||||
export { InquiryStatusBadge } from './InquiryStatusBadge'
|
|
||||||
export { RelatedPropertyCardPanel } from './RelatedPropertyCardPanel'
|
export { RelatedPropertyCardPanel } from './RelatedPropertyCardPanel'
|
||||||
export { PublicNeedList } from './PublicNeedList'
|
export { PublicNeedList } from './PublicNeedList'
|
||||||
export { PublicNeedCard } from './PublicNeedCard'
|
export { PublicNeedCard } from './PublicNeedCard'
|
||||||
|
|||||||
@@ -6,4 +6,6 @@ export interface AdditionalPropertyMatch {
|
|||||||
rentPricePerSqm: number
|
rentPricePerSqm: number
|
||||||
matchScore: number
|
matchScore: number
|
||||||
imageUrl?: string
|
imageUrl?: string
|
||||||
|
reasons: string[] // top 2–3 positive factors
|
||||||
|
topUncertainty?: string // most significant tradeoff or risk
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,26 @@
|
|||||||
export type ReportObjectFieldKey =
|
export type ReportObjectFieldKey =
|
||||||
| 'title' | 'location' | 'areaSqm' | 'rentPricePerSqm' | 'availabilityDate'
|
// Mandatory (always included)
|
||||||
| 'leaseTerm' | 'breakoutOption' | 'currentTenant' | 'propertyNumber'
|
| 'title' | 'location' | 'mapImageUrl' | 'images'
|
||||||
| 'assetType' | 'floor' | 'parking' | 'publicTransportScore'
|
// Grunddaten
|
||||||
|
| 'areaSqm' | 'rentPricePerSqm' | 'availabilityDate'
|
||||||
|
| 'leaseTerm' | 'breakoutOption' | 'breakoutOptionDate' | 'currentTenant'
|
||||||
|
| 'propertyNumber' | 'assetType'
|
||||||
|
// Miet-/Vertragsdaten
|
||||||
|
| 'floor' | 'parking' | 'fitOut' | 'isBarrierFree'
|
||||||
|
// Technische Hard Facts
|
||||||
| 'ceilingHeightM' | 'loadingDocksCount' | 'powerSupplyKva'
|
| 'ceilingHeightM' | 'loadingDocksCount' | 'powerSupplyKva'
|
||||||
| 'hasServerRoom' | 'isBarrierFree' | 'fitOut'
|
| 'hasServerRoom' | 'roomHeight' | 'floorLoad'
|
||||||
|
| 'deliveryAccess' | 'goodsLift' | 'passengerLift' | 'internet'
|
||||||
|
// Soft Factors
|
||||||
| 'prestigeScore' | 'visibilityScore' | 'footfallScore' | 'commuterAccessScore'
|
| 'prestigeScore' | 'visibilityScore' | 'footfallScore' | 'commuterAccessScore'
|
||||||
| 'talentAccessScore' | 'esgScore' | 'flexibilityScore'
|
| 'talentAccessScore' | 'esgScore' | 'flexibilityScore'
|
||||||
| 'expansionPotentialScore' | 'taxEnvironmentScore'
|
| 'expansionPotentialScore' | 'taxEnvironmentScore'
|
||||||
| 'description' | 'images' | 'mapImageUrl' | 'units'
|
| 'microLocation' | 'competitionEnvironment' | 'infrastructure'
|
||||||
|
// Marktinformationen
|
||||||
|
| 'publicTransportScore' | 'marketSignals' | 'negotiationHints' | 'missingData'
|
||||||
|
// Beschreibung & Medien
|
||||||
|
| 'description' | 'units'
|
||||||
|
// Datenqualität
|
||||||
| 'softFactors' | 'dataQuality'
|
| 'softFactors' | 'dataQuality'
|
||||||
|
|
||||||
export interface ReportObjectFieldSelection {
|
export interface ReportObjectFieldSelection {
|
||||||
|
|||||||
@@ -120,12 +120,12 @@ export const matchService = {
|
|||||||
.map(p => {
|
.map(p => {
|
||||||
const match = allMatches.find(m => m.propertyId === p.id)
|
const match = allMatches.find(m => m.propertyId === p.id)
|
||||||
const score = match?.matchScore ?? 0
|
const score = match?.matchScore ?? 0
|
||||||
return { property: p, score }
|
return { property: p, score, match }
|
||||||
})
|
})
|
||||||
.filter(({ score }) => score >= minScore)
|
.filter(({ score }) => score >= minScore)
|
||||||
.sort((a, b) => b.score - a.score)
|
.sort((a, b) => b.score - a.score)
|
||||||
.slice(0, 5)
|
.slice(0, 5)
|
||||||
.map(({ property: p, score }) => ({
|
.map(({ property: p, score, match }) => ({
|
||||||
propertyId: p.id,
|
propertyId: p.id,
|
||||||
title: p.title,
|
title: p.title,
|
||||||
location: `${p.location.city}${p.location.district ? `, ${p.location.district}` : ''}`,
|
location: `${p.location.city}${p.location.district ? `, ${p.location.district}` : ''}`,
|
||||||
@@ -133,6 +133,10 @@ export const matchService = {
|
|||||||
rentPricePerSqm: p.rentPricePerSqm,
|
rentPricePerSqm: p.rentPricePerSqm,
|
||||||
matchScore: score,
|
matchScore: score,
|
||||||
imageUrl: p.images?.[0],
|
imageUrl: p.images?.[0],
|
||||||
|
reasons: (match?.positiveFactors ?? []).slice(0, 3).map(f => f.explanation ?? f.label ?? '').filter(Boolean),
|
||||||
|
topUncertainty: (match?.tradeoffs ?? match?.tradeOffs ?? [])[0]?.description
|
||||||
|
?? (match?.negativeFactors ?? [])[0]?.explanation
|
||||||
|
?? undefined,
|
||||||
}))
|
}))
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user