Compare commits
1 Commits
74f9660581
...
V2
| Author | SHA1 | Date | |
|---|---|---|---|
| 0f8a8ecd2f |
@@ -4,6 +4,14 @@ import type { ParsedNeedCriteria } from '../../domain/needBuilder'
|
||||
import { WEIGHTING_KEYS, WEIGHTING_LABELS } from '../../domain/needBuilder'
|
||||
import type { WeightingKey } from '../../domain/needBuilder'
|
||||
|
||||
function toRaw(w: Record<WeightingKey, number>): Record<WeightingKey, number> {
|
||||
const max = Math.max(...WEIGHTING_KEYS.map(k => w[k] ?? 0))
|
||||
if (max === 0) return Object.fromEntries(WEIGHTING_KEYS.map(k => [k, 3])) as Record<WeightingKey, number>
|
||||
return Object.fromEntries(
|
||||
WEIGHTING_KEYS.map(k => [k, Math.max(1, Math.round(((w[k] ?? 0) / max) * 5))])
|
||||
) as Record<WeightingKey, number>
|
||||
}
|
||||
|
||||
interface Props {
|
||||
criteria: ParsedNeedCriteria
|
||||
weights: Record<WeightingKey, number>
|
||||
@@ -21,13 +29,10 @@ const ASSET_LABELS: Record<string, string> = {
|
||||
const CRITICAL_FIELDS = ['assetType', 'areaRange', 'preferredLocations', 'budgetRange', 'timing']
|
||||
|
||||
export function NeedCardPreview({ criteria: c, weights, confidenceByField, missingFields, needTitle, onNeedTitleChange }: Props) {
|
||||
const maxWeight = Math.max(...WEIGHTING_KEYS.map(k => weights[k] ?? 0), 0.01)
|
||||
|
||||
const fieldEntries = Object.entries(confidenceByField)
|
||||
const overallConfidence = fieldEntries.length > 0
|
||||
? fieldEntries.reduce((sum, [, v]) => sum + v, 0) / fieldEntries.length
|
||||
: 0
|
||||
const lowConfidenceFields = fieldEntries.filter(([, v]) => v < 0.6).map(([k]) => k)
|
||||
const criticalMissing = missingFields.filter(f => CRITICAL_FIELDS.some(cf => f.toLowerCase().includes(cf.toLowerCase())))
|
||||
const isLowConfidence = overallConfidence < 0.6
|
||||
|
||||
@@ -149,35 +154,34 @@ export function NeedCardPreview({ criteria: c, weights, confidenceByField, missi
|
||||
},
|
||||
}}
|
||||
/>
|
||||
{lowConfidenceFields.length > 0 && (
|
||||
<Typography variant="caption" color="text.secondary" sx={{ mt: 0.5, display: 'block' }}>
|
||||
Unsichere Felder: {lowConfidenceFields.join(', ')}
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
<Divider sx={{ my: 2 }} />
|
||||
|
||||
{/* Weights */}
|
||||
<Typography variant="caption" sx={{ fontWeight: 600, display: 'block', mb: 1 }} color="text.secondary">
|
||||
Gewichtungsprofil
|
||||
</Typography>
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.75 }}>
|
||||
{WEIGHTING_KEYS.map(k => {
|
||||
const pct = Math.round((weights[k] ?? 0) * 100)
|
||||
return (
|
||||
<Box key={k} sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
<Typography variant="caption" sx={{ width: 130, flexShrink: 0 }}>{WEIGHTING_LABELS[k]}</Typography>
|
||||
<LinearProgress
|
||||
variant="determinate"
|
||||
value={((weights[k] ?? 0) / maxWeight) * 100}
|
||||
sx={{ flex: 1, height: 6, borderRadius: 3, bgcolor: '#e2e8f0', '& .MuiLinearProgress-bar': { bgcolor: '#1e3a5f' } }}
|
||||
/>
|
||||
<Typography variant="caption" sx={{ width: 32, textAlign: 'right' }}>{pct}%</Typography>
|
||||
</Box>
|
||||
)
|
||||
})}
|
||||
</Box>
|
||||
{/* Priority groups */}
|
||||
{(() => {
|
||||
const rawW = toRaw(weights)
|
||||
const groups = [
|
||||
{ label: 'Kritisch', color: '#c0392b', keys: WEIGHTING_KEYS.filter(k => rawW[k] >= 4) },
|
||||
{ label: 'Wichtig', color: '#1e3a5f', keys: WEIGHTING_KEYS.filter(k => rawW[k] === 3) },
|
||||
{ label: 'Optional', color: '#94a3b8', keys: WEIGHTING_KEYS.filter(k => rawW[k] <= 2) },
|
||||
].filter(g => g.keys.length > 0)
|
||||
return (
|
||||
<>
|
||||
<Typography variant="caption" sx={{ fontWeight: 600, display: 'block', mb: 1 }} color="text.secondary">
|
||||
Prioritäten
|
||||
</Typography>
|
||||
{groups.map(g => (
|
||||
<Box key={g.label} sx={{ display: 'flex', alignItems: 'flex-start', gap: 1, mb: 0.5 }}>
|
||||
<Box sx={{ width: 8, height: 8, borderRadius: '50%', bgcolor: g.color, mt: 0.625, flexShrink: 0 }} />
|
||||
<Typography variant="caption">
|
||||
<strong>{g.label}:</strong>{' '}{g.keys.map(k => WEIGHTING_LABELS[k]).join(', ')}
|
||||
</Typography>
|
||||
</Box>
|
||||
))}
|
||||
</>
|
||||
)
|
||||
})()}
|
||||
</Card>
|
||||
</Box>
|
||||
)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useState } from 'react'
|
||||
import { Box, Card, Chip, Stack, TextField, Typography } from '@mui/material'
|
||||
import { Box, Button, Card, Chip, Collapse, Stack, TextField, Typography } from '@mui/material'
|
||||
import { ChevronDown, ChevronUp } from 'lucide-react'
|
||||
import type { ParsedNeedCriteria } from '../../domain/needBuilder'
|
||||
import { AssetType } from '../../domain/enums'
|
||||
|
||||
@@ -29,6 +30,9 @@ export function NeedInput({ criteria: c, onCriteriaChange: set }: Props) {
|
||||
const [locationDraft, setLocationDraft] = useState('')
|
||||
const [mustHaveDraft, setMustHaveDraft] = useState('')
|
||||
|
||||
const hasDetails = !!c.budgetRange?.maxPerSqm || !!c.timing?.earliestMoveIn || (c.mustHaveCriteria?.length ?? 0) > 0
|
||||
const [showDetails, setShowDetails] = useState(hasDetails)
|
||||
|
||||
function addLocations(raw: string) {
|
||||
const tokens = raw.split(',').map(x => x.trim()).filter(Boolean)
|
||||
if (!tokens.length) return
|
||||
@@ -101,55 +105,67 @@ export function NeedInput({ criteria: c, onCriteriaChange: set }: Props) {
|
||||
sx={{ mb: 0.75 }}
|
||||
/>
|
||||
{(c.preferredLocations?.length ?? 0) > 0 ? (
|
||||
<Stack direction="row" spacing={0.5} sx={{ flexWrap: 'wrap', gap: 0.5, mb: 2.5 }}>
|
||||
<Stack direction="row" spacing={0.5} sx={{ flexWrap: 'wrap', gap: 0.5, mb: 1.5 }}>
|
||||
{c.preferredLocations!.map(loc => (
|
||||
<Chip key={loc} label={loc} size="small"
|
||||
onDelete={() => set({ ...c, preferredLocations: c.preferredLocations!.filter(l => l !== loc) })}
|
||||
/>
|
||||
))}
|
||||
</Stack>
|
||||
) : <Box sx={{ mb: 2.5 }} />}
|
||||
) : <Box sx={{ mb: 1.5 }} />}
|
||||
|
||||
{/* Budget */}
|
||||
<FieldLabel>Budget (max CHF/m²)</FieldLabel>
|
||||
<TextField
|
||||
size="small" type="number" placeholder="z.B. 45"
|
||||
value={c.budgetRange?.maxPerSqm || ''}
|
||||
onChange={e => set({ ...c, budgetRange: { maxPerSqm: parseInt(e.target.value) || 0, currency: 'CHF' } })}
|
||||
sx={{ width: 160, mb: 2.5 }}
|
||||
slotProps={{ htmlInput: { min: 0 } }}
|
||||
/>
|
||||
|
||||
{/* Timing */}
|
||||
<FieldLabel>Verfügbar ab</FieldLabel>
|
||||
<TextField
|
||||
{/* Details toggle */}
|
||||
<Button
|
||||
size="small"
|
||||
placeholder="z.B. Q3 2025 oder 01.09.2025"
|
||||
value={c.timing?.earliestMoveIn ?? ''}
|
||||
onChange={e => set({ ...c, timing: { earliestMoveIn: e.target.value, latestMoveIn: e.target.value, flexibleTiming: true } })}
|
||||
sx={{ width: 220, mb: 2.5 }}
|
||||
/>
|
||||
onClick={() => setShowDetails(v => !v)}
|
||||
endIcon={showDetails ? <ChevronUp size={14} /> : <ChevronDown size={14} />}
|
||||
sx={{ textTransform: 'none', color: '#64748b', pl: 0, mb: 1 }}
|
||||
>
|
||||
{showDetails ? 'Details ausblenden' : 'Budget, Timing & Must-haves'}
|
||||
</Button>
|
||||
|
||||
{/* Must-haves */}
|
||||
<FieldLabel>Must-haves</FieldLabel>
|
||||
<TextField
|
||||
size="small" fullWidth
|
||||
placeholder="z.B. ÖV-Anbindung, Parkplätze — Enter zum Hinzufügen"
|
||||
value={mustHaveDraft}
|
||||
onChange={e => setMustHaveDraft(e.target.value)}
|
||||
onKeyDown={e => { if (e.key === 'Enter' && mustHaveDraft.trim()) addMustHaves(mustHaveDraft) }}
|
||||
onBlur={() => { if (mustHaveDraft.trim()) addMustHaves(mustHaveDraft) }}
|
||||
sx={{ mb: 0.75 }}
|
||||
/>
|
||||
{(c.mustHaveCriteria?.length ?? 0) > 0 && (
|
||||
<Stack direction="row" spacing={0.5} sx={{ flexWrap: 'wrap', gap: 0.5 }}>
|
||||
{c.mustHaveCriteria!.map(item => (
|
||||
<Chip key={item} label={item} size="small"
|
||||
onDelete={() => set({ ...c, mustHaveCriteria: c.mustHaveCriteria!.filter(m => m !== item) })}
|
||||
/>
|
||||
))}
|
||||
</Stack>
|
||||
)}
|
||||
<Collapse in={showDetails}>
|
||||
{/* Budget */}
|
||||
<FieldLabel>Budget (max CHF/m²)</FieldLabel>
|
||||
<TextField
|
||||
size="small" type="number" placeholder="z.B. 45"
|
||||
value={c.budgetRange?.maxPerSqm || ''}
|
||||
onChange={e => set({ ...c, budgetRange: { maxPerSqm: parseInt(e.target.value) || 0, currency: 'CHF' } })}
|
||||
sx={{ width: 160, mb: 2.5 }}
|
||||
slotProps={{ htmlInput: { min: 0 } }}
|
||||
/>
|
||||
|
||||
{/* Timing */}
|
||||
<FieldLabel>Verfügbar ab</FieldLabel>
|
||||
<TextField
|
||||
size="small"
|
||||
placeholder="z.B. Q3 2025 oder 01.09.2025"
|
||||
value={c.timing?.earliestMoveIn ?? ''}
|
||||
onChange={e => set({ ...c, timing: { earliestMoveIn: e.target.value, latestMoveIn: e.target.value, flexibleTiming: true } })}
|
||||
sx={{ width: 220, mb: 2.5 }}
|
||||
/>
|
||||
|
||||
{/* Must-haves */}
|
||||
<FieldLabel>Must-haves</FieldLabel>
|
||||
<TextField
|
||||
size="small" fullWidth
|
||||
placeholder="z.B. ÖV-Anbindung, Parkplätze — Enter zum Hinzufügen"
|
||||
value={mustHaveDraft}
|
||||
onChange={e => setMustHaveDraft(e.target.value)}
|
||||
onKeyDown={e => { if (e.key === 'Enter' && mustHaveDraft.trim()) addMustHaves(mustHaveDraft) }}
|
||||
onBlur={() => { if (mustHaveDraft.trim()) addMustHaves(mustHaveDraft) }}
|
||||
sx={{ mb: 0.75 }}
|
||||
/>
|
||||
{(c.mustHaveCriteria?.length ?? 0) > 0 && (
|
||||
<Stack direction="row" spacing={0.5} sx={{ flexWrap: 'wrap', gap: 0.5 }}>
|
||||
{c.mustHaveCriteria!.map(item => (
|
||||
<Chip key={item} label={item} size="small"
|
||||
onDelete={() => set({ ...c, mustHaveCriteria: c.mustHaveCriteria!.filter(m => m !== item) })}
|
||||
/>
|
||||
))}
|
||||
</Stack>
|
||||
)}
|
||||
</Collapse>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useState } from 'react'
|
||||
import { Box, Button, Card, Slider, Typography } from '@mui/material'
|
||||
import { Box, Button, Card, Chip, Typography } from '@mui/material'
|
||||
import { RotateCcw } from 'lucide-react'
|
||||
import { WEIGHTING_KEYS, WEIGHTING_LABELS } from '../../domain/needBuilder'
|
||||
import type { WeightingKey } from '../../domain/needBuilder'
|
||||
@@ -11,7 +11,11 @@ interface Props {
|
||||
assetType?: string
|
||||
}
|
||||
|
||||
const IMPORTANCE_LABELS = ['', 'Unwichtig', 'Wenig wichtig', 'Wichtig', 'Sehr wichtig', 'Entscheidend']
|
||||
const PRIORITY_LEVELS = [
|
||||
{ label: 'Optional', value: 1, selectedColor: '#64748b' },
|
||||
{ label: 'Wichtig', value: 3, selectedColor: '#1e3a5f' },
|
||||
{ label: 'Kritisch', value: 5, selectedColor: '#c0392b' },
|
||||
] as const
|
||||
|
||||
function toRaw(w: Record<WeightingKey, number>): Record<WeightingKey, number> {
|
||||
const max = Math.max(...WEIGHTING_KEYS.map(k => w[k] ?? 0))
|
||||
@@ -31,7 +35,7 @@ function rawToWeights(raw: Record<WeightingKey, number>): Record<WeightingKey, n
|
||||
export function WeightingEditor({ weights, onChange, assetType }: Props) {
|
||||
const [raw, setRaw] = useState<Record<WeightingKey, number>>(() => toRaw(weights))
|
||||
|
||||
function handleSlider(key: WeightingKey, value: number) {
|
||||
function handleSelect(key: WeightingKey, value: number) {
|
||||
const updated = { ...raw, [key]: value }
|
||||
setRaw(updated)
|
||||
onChange(rawToWeights(updated))
|
||||
@@ -51,7 +55,7 @@ export function WeightingEditor({ weights, onChange, assetType }: Props) {
|
||||
Wichtigkeit der Kriterien
|
||||
</Typography>
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
Schieber nach rechts = wichtiger. Gewichtung wird automatisch berechnet.
|
||||
Wählen Sie für jedes Kriterium die Priorität.
|
||||
</Typography>
|
||||
</Box>
|
||||
<Button
|
||||
@@ -65,32 +69,27 @@ export function WeightingEditor({ weights, onChange, assetType }: Props) {
|
||||
</Box>
|
||||
|
||||
<Card sx={{ p: 3 }}>
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2.5 }}>
|
||||
{WEIGHTING_KEYS.map(key => {
|
||||
const importance = raw[key] ?? 3
|
||||
return (
|
||||
<Box key={key}>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 0.5 }}>
|
||||
<Typography variant="body2" sx={{ fontWeight: 500 }}>
|
||||
{WEIGHTING_LABELS[key]}
|
||||
</Typography>
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
{IMPORTANCE_LABELS[importance]}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Slider
|
||||
value={importance}
|
||||
min={1}
|
||||
max={5}
|
||||
step={1}
|
||||
marks
|
||||
onChange={(_, v) => handleSlider(key, v as number)}
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
|
||||
{WEIGHTING_KEYS.map(key => (
|
||||
<Box key={key} sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
<Typography variant="body2" sx={{ fontWeight: 500, flex: 1 }}>
|
||||
{WEIGHTING_LABELS[key]}
|
||||
</Typography>
|
||||
{PRIORITY_LEVELS.map(p => (
|
||||
<Chip
|
||||
key={p.value}
|
||||
label={p.label}
|
||||
size="small"
|
||||
sx={{ color: '#1e3a5f' }}
|
||||
clickable
|
||||
onClick={() => handleSelect(key, p.value)}
|
||||
sx={raw[key] === p.value
|
||||
? { bgcolor: p.selectedColor, color: 'white', fontWeight: 700, border: 'none' }
|
||||
: { bgcolor: 'transparent', border: '1px solid #e2e8f0', color: '#64748b' }
|
||||
}
|
||||
/>
|
||||
</Box>
|
||||
)
|
||||
})}
|
||||
))}
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
</Card>
|
||||
</Box>
|
||||
|
||||
@@ -44,10 +44,10 @@ export function MatchCardCompact({ vm }: Props) {
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{/* Header: score → type → confidence → availability → risk */}
|
||||
<MatchCardHeader vm={vm} compact />
|
||||
{/* Header: type chip + risk chip + score (secondary) */}
|
||||
<MatchCardHeader vm={vm} />
|
||||
|
||||
{/* Title + location (max 2 lines) */}
|
||||
{/* Title + location + hero narrative */}
|
||||
<Box sx={{ mt: 1.25, mb: 1.25 }}>
|
||||
<Typography variant="subtitle1" sx={{ fontWeight: 600 }} noWrap>
|
||||
{vm.title}
|
||||
@@ -58,12 +58,17 @@ export function MatchCardCompact({ vm }: Props) {
|
||||
{vm.locationLabel}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Typography variant="caption" color="text.secondary" sx={{ mt: 0.25, display: 'block' }}>
|
||||
{[vm.availabilityLabel, `${Math.round(vm.confidenceScore * 100)}% Konfidenz`].filter(Boolean).join(' · ')}
|
||||
</Typography>
|
||||
{vm.explainabilitySummary && (
|
||||
<Typography
|
||||
variant="body2"
|
||||
color="text.secondary"
|
||||
sx={{
|
||||
mt: 0.5,
|
||||
mt: 0.75,
|
||||
fontWeight: 500,
|
||||
color: '#1e293b',
|
||||
lineHeight: 1.5,
|
||||
overflow: 'hidden',
|
||||
display: '-webkit-box',
|
||||
WebkitLineClamp: 2,
|
||||
|
||||
@@ -41,19 +41,18 @@ export function MatchCardExpanded({ vm }: Props) {
|
||||
<MapPin size={14} color="#64748b" />
|
||||
<Typography variant="body2" color="text.secondary">{vm.locationLabel}</Typography>
|
||||
</Box>
|
||||
{vm.availabilityLabel && (
|
||||
<Typography variant="body2" color="text.secondary" sx={{ mt: 0.25 }}>
|
||||
Verfügbar: {vm.availabilityLabel}
|
||||
</Typography>
|
||||
)}
|
||||
<Typography variant="caption" color="text.secondary" sx={{ mt: 0.25, display: 'block' }}>
|
||||
{[vm.availabilityLabel, `${Math.round(vm.confidenceScore * 100)}% Konfidenz`].filter(Boolean).join(' · ')}
|
||||
</Typography>
|
||||
{vm.explainabilitySummary && (
|
||||
<Typography
|
||||
variant="body2"
|
||||
color="text.secondary"
|
||||
sx={{ mt: 1, p: 1.5, bgcolor: '#f8fafc', borderRadius: 1, borderLeft: '3px solid #e2e8f0' }}
|
||||
>
|
||||
{vm.explainabilitySummary}
|
||||
</Typography>
|
||||
<Box sx={{ mt: 1.5 }}>
|
||||
<Typography variant="overline" sx={{ color: '#64748b', display: 'block', mb: 0.5 }}>
|
||||
Strategische Einschätzung
|
||||
</Typography>
|
||||
<Typography variant="body1" sx={{ fontWeight: 500, color: '#1e293b', lineHeight: 1.6 }}>
|
||||
{vm.explainabilitySummary}
|
||||
</Typography>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
|
||||
@@ -8,53 +8,34 @@ const RESULT_TYPE_META: Record<string, { label: string; color: string }> = {
|
||||
FUTURE_AVAILABILITY: { label: 'Zukunftssignal', color: '#7c3aed' },
|
||||
}
|
||||
|
||||
function confidenceColor(score: number): string {
|
||||
if (score >= 0.75) return '#1a7a4a'
|
||||
if (score >= 0.55) return '#d97706'
|
||||
return '#c0392b'
|
||||
}
|
||||
|
||||
interface Props {
|
||||
vm: MatchCardViewModel
|
||||
compact?: boolean
|
||||
}
|
||||
|
||||
export function MatchCardHeader({ vm, compact }: Props) {
|
||||
export function MatchCardHeader({ vm }: Props) {
|
||||
const rt = RESULT_TYPE_META[vm.resultType] ?? { label: vm.resultType, color: '#64748b' }
|
||||
const confPct = Math.round(vm.confidenceScore * 100)
|
||||
|
||||
const topRisk = vm.risks.length > 0 ? vm.risks[0].level : undefined
|
||||
const showRiskBadge = topRisk && topRisk !== 'LOW'
|
||||
const riskLabel = topRisk === 'CRITICAL' ? 'Kritisch' : topRisk === 'HIGH' ? 'Hohes Risiko' : 'Mittleres Risiko'
|
||||
const riskColor: 'error' | 'warning' = (topRisk === 'HIGH' || topRisk === 'CRITICAL') ? 'error' : 'warning'
|
||||
const showRiskBadge = topRisk === 'CRITICAL' || topRisk === 'HIGH'
|
||||
const riskLabel = topRisk === 'CRITICAL' ? 'Kritisch' : 'Hohes Risiko'
|
||||
const riskColor: 'error' = 'error'
|
||||
|
||||
return (
|
||||
<Box sx={{ display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between', gap: 1, flexWrap: 'wrap' }}>
|
||||
{/* Score — leftmost, most prominent */}
|
||||
<MatchScoreDisplay score={vm.matchScore} size={compact ? 'sm' : 'md'} />
|
||||
|
||||
{/* Badges: resultType → assetType → confidence → availability → risk */}
|
||||
{/* Badges: resultType + risk only */}
|
||||
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 0.5, alignItems: 'center' }}>
|
||||
<Chip
|
||||
label={rt.label}
|
||||
size="small"
|
||||
sx={{ bgcolor: rt.color, color: 'white', fontWeight: 600, fontSize: 11 }}
|
||||
/>
|
||||
{vm.assetType && (
|
||||
<Chip label={vm.assetType} size="small" variant="outlined" sx={{ fontSize: 11 }} />
|
||||
)}
|
||||
<Chip
|
||||
label={`${confPct}% Konfidenz`}
|
||||
size="small"
|
||||
sx={{ bgcolor: confidenceColor(vm.confidenceScore), color: 'white', fontSize: 11 }}
|
||||
/>
|
||||
{vm.availabilityLabel && (
|
||||
<Chip label={vm.availabilityLabel} size="small" variant="outlined" sx={{ fontSize: 11 }} />
|
||||
)}
|
||||
{showRiskBadge && (
|
||||
<Chip label={riskLabel} size="small" color={riskColor} variant="outlined" />
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{/* Score — secondary, right side */}
|
||||
<MatchScoreDisplay score={vm.matchScore} size="sm" />
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -11,10 +11,10 @@ export function ResultFeedHeader({ total, verifiedCount, externalCount, futureCo
|
||||
return (
|
||||
<Box sx={{ bgcolor: 'white', borderBottom: '1px solid #e2e8f0', px: 3, py: 2 }}>
|
||||
<Typography variant="h5" sx={{ fontWeight: 700 }} color="text.primary">
|
||||
{total} Treffer gefunden
|
||||
{total} Empfehlungen
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
{verifiedCount} Verified · {externalCount} Extern · {futureCount} Signale
|
||||
{verifiedCount} Verifiziert · {externalCount} Extern · {futureCount} Marktsignale
|
||||
</Typography>
|
||||
</Box>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user