refactor: split large page components + taxonomy/HeatBadge/FutureAvailability improvements

- Taxonomy: merge VERIFIED_PORTFOLIO + EXTERNAL_MARKET display → 'Plattform' (dark blue) across all surfaces
- HeatBadge: new flame indicator for hot properties (grid, list, pipeline views)
- FutureAvailabilityContextPanel: richer detail page with AI summary, strategic assessment, sources
- Refactor Pipeline.tsx (630→152 lines) → pipeline/PipelineCard, PipelineColumn, PipelineDetailPanel, pipelineConstants, pipelineUtils
- Refactor IntelligenceMatchCard.tsx (483→179 lines) → FutureAvailabilityCard extracted
- Refactor MatchDetail.tsx (559→464 lines) → useMatchDetailData hook, MatchDetailPropertyDetails
- Refactor Compare.tsx (638→485 lines) → compareUtils, CompareCriteriaCard extracted

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Benjamin Sutter
2026-05-23 23:36:03 +02:00
parent 72e4f08900
commit 22c195b4a5
28 changed files with 1615 additions and 1282 deletions
+51
View File
@@ -0,0 +1,51 @@
import type { PipelineItem } from '../../domain/pipeline'
export function scoreColor(score: number) {
return score >= 80 ? '#1a7a4a' : score >= 65 ? '#d97706' : '#c0392b'
}
export function detailPath(item: PipelineItem): string | null {
// propertyId is always stable across sessions — prefer it
if (item.propertyId) return `/demand/property/${item.propertyId}`
// matchId / UUID only works in the same session (matchStore is ephemeral)
if (item.matchId) return `/demand/results/${item.matchId}`
if (item.id.startsWith('match-')) return `/demand/results/${item.id}`
return null
}
export function getKiInsight(item: PipelineItem): { summary: string; positives: string[]; risks: string[] } {
if (item.stage === 'SAVED') return {
summary: `Merkliste-Eintrag mit ${item.matchScore}% Match. Prüfen Sie, ob dieses Objekt qualifiziert werden soll.`,
positives: [`Match ${item.matchScore}%`],
risks: ['Noch nicht qualifiziert'],
}
if (item.stage === 'CLOSED_WON') return {
summary: `Abschluss erfolgreich. ${item.title} wurde zu ${item.matchScore}% Match abgeschlossen.`,
positives: ['Vertraglich gesichert', `Match ${item.matchScore}%`, 'Alle Kriterien erfüllt'],
risks: [],
}
if (item.stage === 'CLOSED_LOST') return {
summary: item.notes ?? 'Objekt nicht realisiert.',
positives: [],
risks: ['Nicht verfügbar', 'Alternative Optionen prüfen'],
}
const s = item.matchScore
return {
summary: s >= 80
? `Starkes Objekt (${s}%) — deckt die wesentlichen Suchkriterien ab. Prozess aktiv weitertreiben.`
: s >= 65
? `Solides Objekt (${s}%) mit Potenzial. Gezielte Klärung offener Punkte empfohlen.`
: `Schwächerer Match (${s}%). Abweichungen kritisch prüfen bevor weitere Ressourcen investiert werden.`,
positives: [
...(s >= 80 ? [`Match ${s}% — hohe Übereinstimmung`] : []),
...(item.areaLabel ? [`Fläche: ${item.areaLabel}`] : []),
...(item.resultType === 'VERIFIED_PORTFOLIO' ? ['Geprüftes Portfolio-Objekt'] : []),
...(item.stage === 'NEGOTIATION' ? ['Verhandlung läuft — kurz vor Abschluss'] : []),
].slice(0, 3),
risks: [
...(s < 80 ? [`Match ${s}% — Abweichungen prüfen`] : []),
...(item.notes?.includes('Budget') ? ['Budget-Diskrepanz erwähnt'] : []),
...(item.resultType === 'FUTURE_AVAILABILITY' ? ['Verfügbarkeit noch nicht bestätigt'] : []),
].slice(0, 2),
}
}