feat: role-aware UX, market intelligence, score fix & layout scroll
- Role-based workspace access: Property Manager gets Supply+Demand, Operations restricted to Super Admin + Reviewer only - Root redirect routes each persona to their first workspace - Match Center: replaced 3-panel with auto-sorted flat list + drawer - AI Search: unified form with dual-action (Jetzt suchen / Als Suchprofil speichern) - CompareTray hidden on non-Demand routes; Vergleichen removed from Supply - LocationIntelligencePanel: city KPIs, rent trends, soft factors, comparables - NegotiationInsightsPanel: price positioning, active demand, selling arguments - scoreCalculator: cap hardMatchScore and softFactorScore to max 100 - Layout: add display:flex to overflow:hidden wrappers so inner scroll works (MatchCenter drawer, MarketIntelligence, SourceMonitoring, SignalPipeline) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,131 @@
|
||||
import { Box, Button, Chip, Typography } from '@mui/material'
|
||||
import { MatchStatusBadge } from './MatchStatusBadge'
|
||||
import type { Match } from '../../domain/match'
|
||||
import type { Property } from '../../domain/property'
|
||||
import type { Need } from '../../domain/need'
|
||||
|
||||
const STRENGTH_META: Record<string, { label: string; bg: string; color: string }> = {
|
||||
STRONG: { label: 'Stark', bg: '#f0fdf4', color: '#1a7a4a' },
|
||||
MODERATE: { label: 'Mittel', bg: '#fefce8', color: '#d97706' },
|
||||
WEAK: { label: 'Schwach', bg: '#fff1f2', color: '#c0392b' },
|
||||
}
|
||||
|
||||
function scoreColor(score: number) {
|
||||
if (score >= 80) return '#1a7a4a'
|
||||
if (score >= 60) return '#d97706'
|
||||
return '#c0392b'
|
||||
}
|
||||
|
||||
interface Props {
|
||||
match: Match
|
||||
property: Property | undefined
|
||||
need: Need | undefined
|
||||
onSelect: () => void
|
||||
onApprove: () => void
|
||||
}
|
||||
|
||||
export function MatchListCard({ match, property, need, onSelect, onApprove }: Props) {
|
||||
const strength = STRENGTH_META[match.matchStrength] ?? { label: match.matchStrength, bg: '#f1f5f9', color: '#64748b' }
|
||||
const summary = match.explainabilitySummary ?? ''
|
||||
|
||||
return (
|
||||
<Box
|
||||
onClick={onSelect}
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 2,
|
||||
px: 3,
|
||||
py: 2,
|
||||
borderBottom: '1px solid #e2e8f0',
|
||||
bgcolor: 'white',
|
||||
cursor: 'pointer',
|
||||
'&:hover': { bgcolor: '#f8fafc' },
|
||||
}}
|
||||
>
|
||||
{/* Score bubble */}
|
||||
<Box
|
||||
sx={{
|
||||
width: 52,
|
||||
height: 52,
|
||||
borderRadius: 2,
|
||||
bgcolor: scoreColor(match.matchScore),
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
flexShrink: 0,
|
||||
}}
|
||||
>
|
||||
<Typography variant="h6" sx={{ color: 'white', fontWeight: 700, lineHeight: 1 }}>
|
||||
{match.matchScore}
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
{/* Property + need */}
|
||||
<Box sx={{ flex: 1, minWidth: 0 }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 0.25, flexWrap: 'wrap' }}>
|
||||
<Typography variant="body2" sx={{ fontWeight: 600, whiteSpace: 'nowrap' }}>
|
||||
{property?.title ?? match.propertyId}
|
||||
</Typography>
|
||||
<MatchStatusBadge status={match.status} />
|
||||
</Box>
|
||||
<Typography variant="caption" color="text.secondary" sx={{ display: 'block' }}>
|
||||
{[property?.location.city, property?.areaSqm ? `${property.areaSqm} m²` : null].filter(Boolean).join(' · ')}
|
||||
</Typography>
|
||||
<Box sx={{ display: 'flex', gap: 0.75, mt: 0.5, flexWrap: 'wrap' }}>
|
||||
{need && (
|
||||
<Chip
|
||||
label={need.companyName}
|
||||
size="small"
|
||||
sx={{ bgcolor: '#eff6ff', color: '#1e3a5f', fontSize: 11, height: 20, fontWeight: 500 }}
|
||||
/>
|
||||
)}
|
||||
<Chip
|
||||
label={strength.label}
|
||||
size="small"
|
||||
sx={{ bgcolor: strength.bg, color: strength.color, fontSize: 11, height: 20 }}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* Summary excerpt */}
|
||||
{summary && (
|
||||
<Typography
|
||||
variant="caption"
|
||||
color="text.secondary"
|
||||
sx={{
|
||||
maxWidth: 220,
|
||||
display: { xs: 'none', lg: 'block' },
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
}}
|
||||
>
|
||||
{summary.length > 90 ? summary.slice(0, 90) + '…' : summary}
|
||||
</Typography>
|
||||
)}
|
||||
|
||||
{/* Actions */}
|
||||
<Box sx={{ display: 'flex', gap: 0.75, flexShrink: 0 }} onClick={e => e.stopPropagation()}>
|
||||
{match.status !== 'APPROVED' && (
|
||||
<Button
|
||||
size="small"
|
||||
variant="contained"
|
||||
onClick={onApprove}
|
||||
sx={{ textTransform: 'none', bgcolor: '#1a7a4a', '&:hover': { bgcolor: '#15643c' } }}
|
||||
>
|
||||
Genehmigen
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
size="small"
|
||||
variant="outlined"
|
||||
onClick={onSelect}
|
||||
sx={{ textTransform: 'none' }}
|
||||
>
|
||||
Details
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user