feat(F020): frontend infrastructure foundation
- src/lib/constants.ts: centralized route paths, labels, thresholds - src/lib/utils.ts: formatting, color helpers, math utils - src/hooks/: useProperties, useMatches, useFutureSignals, useNeeds - src/components/scores/MatchScoreRing - src/components/data-quality/DataQualityBar - src/components/future-signals/SignalTypeBadge - src/components/cards/SourceTypeBadge - src/provider/AuthProvider (placeholder, swappable) - main.tsx: AuthProvider + centralized stale time Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,113 @@
|
||||
// Centralized app-wide constants — no magic strings anywhere else
|
||||
|
||||
export const APP_NAME = 'Property-Match'
|
||||
export const APP_VERSION = '0.1.0'
|
||||
|
||||
// Organisation
|
||||
export const DEFAULT_ORG_ID = 'org-wincasa'
|
||||
|
||||
// Pagination
|
||||
export const DEFAULT_PAGE_SIZE = 25
|
||||
export const MAX_COMPARE_ITEMS = 3
|
||||
|
||||
// Data quality thresholds
|
||||
export const DQ_HIGH = 0.8
|
||||
export const DQ_MEDIUM = 0.6
|
||||
|
||||
// Confidence thresholds
|
||||
export const CONF_HIGH = 0.85
|
||||
export const CONF_MEDIUM = 0.65
|
||||
|
||||
// Match score thresholds (0–100)
|
||||
export const SCORE_STRONG = 80
|
||||
export const SCORE_MODERATE = 60
|
||||
|
||||
// Probability thresholds (future signals)
|
||||
export const PROB_HIGH = 0.7
|
||||
export const PROB_MEDIUM = 0.5
|
||||
|
||||
// Query stale times (ms)
|
||||
export const STALE_PROPERTIES = 5 * 60 * 1000
|
||||
export const STALE_MATCHES = 2 * 60 * 1000
|
||||
export const STALE_SIGNALS = 5 * 60 * 1000
|
||||
|
||||
// Route paths — single source of truth
|
||||
export const ROUTES = {
|
||||
HOME: '/',
|
||||
SUPPLY: {
|
||||
DASHBOARD: '/supply/dashboard',
|
||||
PROPERTIES: '/supply/properties',
|
||||
MATCH_CENTER: '/supply/match-center',
|
||||
FUTURE_AVAILABILITY: '/supply/future-availability',
|
||||
DATA_QUALITY: '/supply/data-quality',
|
||||
},
|
||||
DEMAND: {
|
||||
AI_SEARCH: '/demand/ai-search',
|
||||
RESULTS: '/demand/results',
|
||||
COMPARE: '/demand/compare',
|
||||
SHORTLISTS: '/demand/shortlists',
|
||||
},
|
||||
OPS: {
|
||||
REVIEW_QUEUE: '/ops/review-queue',
|
||||
AI_MONITORING: '/ops/ai-monitoring',
|
||||
GOVERNANCE: '/ops/governance',
|
||||
},
|
||||
} as const
|
||||
|
||||
// Asset type display labels
|
||||
export const ASSET_TYPE_LABELS: Record<string, string> = {
|
||||
OFFICE: 'Büro',
|
||||
RETAIL: 'Retail',
|
||||
GASTRO: 'Gastronomie',
|
||||
LOGISTICS: 'Logistik',
|
||||
PRODUCTION: 'Produktion',
|
||||
MIXED: 'Gemischt',
|
||||
}
|
||||
|
||||
// Result type display labels
|
||||
export const RESULT_TYPE_LABELS: Record<string, string> = {
|
||||
VERIFIED_PORTFOLIO: 'Verified Portfolio',
|
||||
EXTERNAL_MARKET: 'Marktinserat',
|
||||
FUTURE_AVAILABILITY: 'Zukunftssignal',
|
||||
}
|
||||
|
||||
// Match strength display labels
|
||||
export const MATCH_STRENGTH_LABELS: Record<string, string> = {
|
||||
STRONG: 'Stark',
|
||||
MODERATE: 'Mittel',
|
||||
WEAK: 'Schwach',
|
||||
}
|
||||
|
||||
// Availability status display labels
|
||||
export const AVAILABILITY_LABELS: Record<string, string> = {
|
||||
AVAILABLE_NOW: 'Verfügbar',
|
||||
AVAILABLE_SOON: 'Bald verfügbar',
|
||||
FUTURE_SIGNAL: 'Zukunftssignal',
|
||||
OCCUPIED: 'Belegt',
|
||||
UNKNOWN: 'Unbekannt',
|
||||
}
|
||||
|
||||
// Risk level display labels
|
||||
export const RISK_LABELS: Record<string, string> = {
|
||||
LOW: 'Niedrig',
|
||||
MEDIUM: 'Mittel',
|
||||
HIGH: 'Hoch',
|
||||
CRITICAL: 'Kritisch',
|
||||
}
|
||||
|
||||
// Signal type display labels
|
||||
export const SIGNAL_TYPE_LABELS: Record<string, string> = {
|
||||
EXPANSION: 'Expansion',
|
||||
POSSIBLE_MOVE_OUT: 'Möglicher Auszug',
|
||||
CONSTRUCTION_PROJECT: 'Bauprojekt',
|
||||
RESTRUCTURING: 'Umstrukturierung',
|
||||
PROJECT_DEVELOPMENT: 'Projektentwicklung',
|
||||
SPACE_CONSOLIDATION: 'Flächenkonsolidierung',
|
||||
}
|
||||
|
||||
// Data freshness display labels
|
||||
export const FRESHNESS_LABELS: Record<string, string> = {
|
||||
FRESH: 'Aktuell',
|
||||
STALE: 'Veraltet',
|
||||
OUTDATED: 'Abgelaufen',
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
import {
|
||||
DQ_HIGH, DQ_MEDIUM,
|
||||
CONF_HIGH, CONF_MEDIUM,
|
||||
SCORE_STRONG, SCORE_MODERATE,
|
||||
PROB_HIGH, PROB_MEDIUM,
|
||||
} from './constants'
|
||||
|
||||
// ── Formatting ────────────────────────────────────────────────────────────────
|
||||
|
||||
export function formatCHF(amount: number, decimals = 0): string {
|
||||
return new Intl.NumberFormat('de-CH', {
|
||||
style: 'currency',
|
||||
currency: 'CHF',
|
||||
minimumFractionDigits: decimals,
|
||||
maximumFractionDigits: decimals,
|
||||
}).format(amount)
|
||||
}
|
||||
|
||||
export function formatArea(sqm: number): string {
|
||||
return `${new Intl.NumberFormat('de-CH').format(sqm)} m²`
|
||||
}
|
||||
|
||||
export function formatPercent(value: number, decimals = 0): string {
|
||||
return `${(value * 100).toFixed(decimals)} %`
|
||||
}
|
||||
|
||||
export function formatDate(iso: string): string {
|
||||
return new Intl.DateTimeFormat('de-CH', { day: '2-digit', month: '2-digit', year: 'numeric' }).format(new Date(iso))
|
||||
}
|
||||
|
||||
export function formatRelativeDate(iso: string): string {
|
||||
const diff = Date.now() - new Date(iso).getTime()
|
||||
const hours = Math.floor(diff / 3_600_000)
|
||||
if (hours < 1) return 'Gerade eben'
|
||||
if (hours < 24) return `vor ${hours} Stunde${hours === 1 ? '' : 'n'}`
|
||||
const days = Math.floor(hours / 24)
|
||||
if (days < 7) return `vor ${days} Tag${days === 1 ? '' : 'en'}`
|
||||
return formatDate(iso)
|
||||
}
|
||||
|
||||
// ── Color helpers (return MUI color token strings) ────────────────────────────
|
||||
|
||||
export function dataQualityColor(score: number): 'success' | 'warning' | 'error' {
|
||||
if (score >= DQ_HIGH) return 'success'
|
||||
if (score >= DQ_MEDIUM) return 'warning'
|
||||
return 'error'
|
||||
}
|
||||
|
||||
export function confidenceColor(score: number): 'success' | 'primary' | 'warning' {
|
||||
if (score >= CONF_HIGH) return 'success'
|
||||
if (score >= CONF_MEDIUM) return 'primary'
|
||||
return 'warning'
|
||||
}
|
||||
|
||||
export function matchScoreColor(score: number): 'success' | 'warning' | 'error' {
|
||||
if (score >= SCORE_STRONG) return 'success'
|
||||
if (score >= SCORE_MODERATE) return 'warning'
|
||||
return 'error'
|
||||
}
|
||||
|
||||
export function probabilityColor(prob: number): 'success' | 'warning' | 'error' {
|
||||
if (prob >= PROB_HIGH) return 'success'
|
||||
if (prob >= PROB_MEDIUM) return 'warning'
|
||||
return 'error'
|
||||
}
|
||||
|
||||
// Hex color variants for use in sx (when MUI color tokens aren't enough)
|
||||
export function dataQualityHex(score: number): string {
|
||||
if (score >= DQ_HIGH) return '#1a7a4a'
|
||||
if (score >= DQ_MEDIUM) return '#d97706'
|
||||
return '#c0392b'
|
||||
}
|
||||
|
||||
export function confidenceHex(score: number): string {
|
||||
if (score >= CONF_HIGH) return '#1a7a4a'
|
||||
if (score >= CONF_MEDIUM) return '#1e3a5f'
|
||||
return '#d97706'
|
||||
}
|
||||
|
||||
export function probabilityHex(prob: number): string {
|
||||
if (prob >= PROB_HIGH) return '#1a7a4a'
|
||||
if (prob >= PROB_MEDIUM) return '#d97706'
|
||||
return '#c0392b'
|
||||
}
|
||||
|
||||
// ── Misc ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
export function clamp(value: number, min: number, max: number): number {
|
||||
return Math.min(Math.max(value, min), max)
|
||||
}
|
||||
|
||||
export function average(values: number[]): number {
|
||||
if (values.length === 0) return 0
|
||||
return values.reduce((a, b) => a + b, 0) / values.length
|
||||
}
|
||||
|
||||
export function truncate(str: string, maxLen: number): string {
|
||||
return str.length <= maxLen ? str : str.slice(0, maxLen - 1) + '…'
|
||||
}
|
||||
Reference in New Issue
Block a user