fix: UX polish — score formula, role badges, compare view, page titles
- Score: remove hidden dataQuality/confidence modifiers from finalScore; formula now correctly shows hard×60% + soft×40% = displayed total - "Ihr Objekt" badge: only shown for PROPERTY_MANAGER/ORGANIZATION_ADMIN in both IntelligenceMatchCard and MatchCardHeader (DEMAND_USER sees Verified Portfolio as a normal listing without ownership indicator) - Compare: fix factor lookup to use allFactors (includes mid-range 45–70 scores) so Prestige, Erreichbarkeit etc. no longer show "Nicht verfügbar" - Compare: richer AI summary with overallAssessment, perPropertyAssessment (strengths/weaknesses/bestFor/keyRisk per property), and recommendation - Compare/Results: pb:10 so CompareTray never overlaps last row of content - AppShell: dynamic route names for /demand/results/:id → "Match Detail" and /demand/property/:id → "Objekt Detail" (no more UUID in header) - MatchDetail: remove "Match EF9" debug chip from sticky nav - LocationIntelligencePanel: comparable listings are now clickable links navigating to /demand/property/:id - Comparable links: blue text + hover highlight Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -53,9 +53,24 @@ export interface ComparisonSummary {
|
||||
biggestTradeoffs: string[]
|
||||
missingDataWarnings: string[]
|
||||
recommendedNextStep: string
|
||||
overallAssessment: string
|
||||
perPropertyAssessment: Array<{
|
||||
matchId: string
|
||||
label: string
|
||||
strengths: string[]
|
||||
weaknesses: string[]
|
||||
bestFor: string
|
||||
keyRisk: string | null
|
||||
}>
|
||||
recommendation: string
|
||||
}
|
||||
|
||||
function buildComparisonSummary(items: UnifiedMatchResult[]): ComparisonSummary {
|
||||
const getTitle = (item: UnifiedMatchResult) =>
|
||||
item.resultType !== 'FUTURE_AVAILABILITY'
|
||||
? (item as any).property?.title ?? `Match ${item.matchScore}`
|
||||
: (item as any).signal?.companyName ?? 'Zukunftssignal'
|
||||
|
||||
if (items.length === 0) {
|
||||
return {
|
||||
strongestOption: { matchId: '', label: '–', reason: 'Keine Ergebnisse' },
|
||||
@@ -64,14 +79,12 @@ function buildComparisonSummary(items: UnifiedMatchResult[]): ComparisonSummary
|
||||
biggestTradeoffs: [],
|
||||
missingDataWarnings: [],
|
||||
recommendedNextStep: 'Suchergebnisse überprüfen',
|
||||
overallAssessment: 'Keine Ergebnisse für die Analyse verfügbar.',
|
||||
perPropertyAssessment: [],
|
||||
recommendation: 'Suchergebnisse überprüfen und erneut versuchen.',
|
||||
}
|
||||
}
|
||||
|
||||
const getTitle = (item: UnifiedMatchResult) =>
|
||||
item.resultType !== 'FUTURE_AVAILABILITY'
|
||||
? (item as any).property?.title ?? `Match ${item.matchScore}`
|
||||
: (item as any).signal?.companyName ?? 'Zukunftssignal'
|
||||
|
||||
const strongest = items.reduce((a, b) => a.matchScore > b.matchScore ? a : b)
|
||||
|
||||
const propertyItems = items.filter(i => i.resultType !== 'FUTURE_AVAILABILITY')
|
||||
@@ -95,6 +108,58 @@ function buildComparisonSummary(items: UnifiedMatchResult[]): ComparisonSummary
|
||||
|
||||
const topNextAction = strongest.match.nextBestActions?.[0]?.label ?? 'Objekt besichtigen oder Details prüfen'
|
||||
|
||||
// Overall assessment
|
||||
const labels = items.map(getTitle)
|
||||
const scoreLeader = getTitle(strongest)
|
||||
const priceLeader = bestValue ? getTitle(bestValue) : null
|
||||
let overallAssessment: string
|
||||
if (items.length === 1) {
|
||||
overallAssessment = `${labels[0]} erfüllt die Kernkriterien mit einem Match Score von ${strongest.matchScore}/100. Eine Vergleichsbasis fehlt — weitere Optionen hinzufügen für eine fundierte Entscheidung.`
|
||||
} else if (priceLeader && priceLeader !== scoreLeader) {
|
||||
overallAssessment = `Beide Optionen erfüllen Standort und Fläche gut. ${scoreLeader} führt beim Match Score und Prestige, ${priceLeader} beim Preis-Leistungs-Verhältnis.`
|
||||
} else {
|
||||
overallAssessment = `${scoreLeader} dominiert in den meisten Kriterien mit dem höchsten Match Score (${strongest.matchScore}/100). Die Optionen unterscheiden sich hauptsächlich in Lage und Ausstattung.`
|
||||
}
|
||||
|
||||
// Per-property assessment
|
||||
const deriveBestFor = (item: UnifiedMatchResult): string => {
|
||||
const assetType = (item as any).property?.assetType ?? ''
|
||||
const score = item.matchScore
|
||||
const hasPosPrestige = item.match.positiveFactors.some(f =>
|
||||
f.criterion.toLowerCase().includes('prestige') || f.criterion.toLowerCase().includes('location')
|
||||
)
|
||||
const hasLowPrice = bestValue?.matchId === item.matchId
|
||||
if (hasPosPrestige && score >= 80) return 'Repräsentationsbedarf mit Prestige-Anforderungen'
|
||||
if (hasLowPrice) return 'Kostenoptimierte Wachstumsphase'
|
||||
if (assetType === 'LOGISTICS' || assetType === 'PRODUCTION') return 'Operative Effizienz und Flächenflexibilität'
|
||||
if (assetType === 'RETAIL') return 'Hohe Kundenfrequenz und Sichtbarkeit'
|
||||
if (score >= 80) return 'Anspruchsvolle Anforderungen mit hoher Matchqualität'
|
||||
return 'Ausgewogenes Preis-Leistungs-Profil'
|
||||
}
|
||||
|
||||
const perPropertyAssessment = items.map(item => {
|
||||
const topStrengths = item.match.positiveFactors
|
||||
.slice(0, 2)
|
||||
.map(f => f.explanation)
|
||||
const topWeaknesses = item.match.negativeFactors.length > 0
|
||||
? item.match.negativeFactors.slice(0, 2).map(f => f.explanation)
|
||||
: ['Keine kritischen Schwächen identifiziert']
|
||||
const keyRisk = item.match.risks?.[0]?.description ?? null
|
||||
return {
|
||||
matchId: item.matchId,
|
||||
label: getTitle(item),
|
||||
strengths: topStrengths,
|
||||
weaknesses: topWeaknesses,
|
||||
bestFor: deriveBestFor(item),
|
||||
keyRisk,
|
||||
}
|
||||
})
|
||||
|
||||
// Recommendation
|
||||
const recommendation = items.length >= 2
|
||||
? `Besichtigung beider Objekte empfohlen — danach Entscheidung auf Basis Mietvertragslaufzeit und Ausbaustandard.`
|
||||
: `Besichtigung von ${getTitle(strongest)} empfohlen — anschliessend Vertragskonditionen und Laufzeit prüfen.`
|
||||
|
||||
return {
|
||||
strongestOption: {
|
||||
matchId: strongest.matchId,
|
||||
@@ -116,6 +181,9 @@ function buildComparisonSummary(items: UnifiedMatchResult[]): ComparisonSummary
|
||||
biggestTradeoffs: tradeoffs,
|
||||
missingDataWarnings: missingWarnings,
|
||||
recommendedNextStep: topNextAction,
|
||||
overallAssessment,
|
||||
perPropertyAssessment,
|
||||
recommendation,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user