feat: F007 property repository & detail view

- useProperties: added usePropertyById, usePropertyMatches, usePropertySignals hooks
- propertyService.getProperties(), matchService.getMatchesForProperty(),
  futureSignalService.getSignalsForProperty() added
- PropertyFilterBar: asset type chips, availability select, sort select, search
- PropertyTable: sortable columns, row selection highlight, loading skeletons,
  empty/error states, Eye/Plus/Bookmark actions
- PropertyCard: decision-object card with header, primary, matchability,
  data quality and action zones; card states (selected, compareSelected,
  stale, low-confidence)
- PropertyDetailView: 7-tab detail panel (Übersicht, Hard Facts, Soft Factors,
  Matchability, Datenqualität, Quelle, Signale) with low-quality banner,
  missing field UX, data update CTA
- PropertyDetailSkeleton: loading state
- Properties page: two-panel layout (table + sliding detail panel)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Benjamin Sutter
2026-05-15 16:53:51 +02:00
parent 7b7be0b436
commit 3cd2e83b25
12 changed files with 1303 additions and 354 deletions
+81
View File
@@ -0,0 +1,81 @@
import type { AssetType, AvailabilityStatus, ResultType } from '../../domain/enums'
export function getAssetTypeLabel(type: AssetType): string {
const labels: Record<string, string> = {
OFFICE: 'Büro',
LOGISTICS: 'Logistik',
RETAIL: 'Retail',
GASTRO: 'Gastro',
PRODUCTION: 'Produktion',
MIXED: 'Gemischt',
LIGHT_INDUSTRIAL: 'Leichtindustrie',
UNKNOWN: 'Unbekannt',
}
return labels[type] ?? type
}
export function getAssetTypeColor(type: AssetType): string {
const colors: Record<string, string> = {
OFFICE: '#1e3a5f',
LOGISTICS: '#d97706',
RETAIL: '#7c3aed',
GASTRO: '#0d9488',
PRODUCTION: '#92400e',
MIXED: '#6b7280',
LIGHT_INDUSTRIAL: '#b45309',
UNKNOWN: '#9ca3af',
}
return colors[type] ?? '#6b7280'
}
export function getAvailabilityLabel(status: AvailabilityStatus): string {
const labels: Record<string, string> = {
AVAILABLE_NOW: 'Verfügbar',
AVAILABLE_SOON: 'Bald verfügbar',
FUTURE_SIGNAL: 'Zukunftssignal',
OCCUPIED: 'Belegt',
UNKNOWN: 'Unbekannt',
}
return labels[status] ?? status
}
export function getAvailabilityChipColor(status: AvailabilityStatus): 'success' | 'warning' | 'secondary' | 'error' | 'default' {
const colors: Record<string, 'success' | 'warning' | 'secondary' | 'error' | 'default'> = {
AVAILABLE_NOW: 'success',
AVAILABLE_SOON: 'warning',
FUTURE_SIGNAL: 'secondary',
OCCUPIED: 'error',
UNKNOWN: 'default',
}
return colors[status] ?? 'default'
}
export function getResultTypeLabel(type: ResultType): string {
const labels: Record<string, string> = {
VERIFIED_PORTFOLIO: 'Portfolio',
EXTERNAL_MARKET: 'Markt',
FUTURE_AVAILABILITY: 'Zukunft',
}
return labels[type] ?? type
}
export function getResultTypeColor(type: ResultType): string {
const colors: Record<string, string> = {
VERIFIED_PORTFOLIO: '#1e3a5f',
EXTERNAL_MARKET: '#1a7a4a',
FUTURE_AVAILABILITY: '#7c3aed',
}
return colors[type] ?? '#6b7280'
}
export function qualityColor(score: number): 'success' | 'warning' | 'error' {
if (score >= 0.8) return 'success'
if (score >= 0.6) return 'warning'
return 'error'
}
export function confidenceColor(score: number): string {
if (score >= 0.85) return '#1a7a4a'
if (score >= 0.65) return '#1e3a5f'
return '#d97706'
}