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:
Benjamin Sutter
2026-05-17 00:58:10 +02:00
parent e13fe470fb
commit efc406ace9
37 changed files with 4871 additions and 459 deletions
+37 -37
View File
@@ -1,3 +1,4 @@
import { useState } from 'react'
import { Box, Button, Card, Slider, Typography } from '@mui/material'
import { RotateCcw } from 'lucide-react'
import { WEIGHTING_KEYS, WEIGHTING_LABELS } from '../../domain/needBuilder'
@@ -10,17 +11,36 @@ interface Props {
assetType?: string
}
export function WeightingEditor({ weights, onChange, assetType }: Props) {
const total = WEIGHTING_KEYS.reduce((sum, k) => sum + (weights[k] ?? 0), 0)
const totalPct = Math.round(total * 100)
const isBalanced = totalPct >= 95 && totalPct <= 105
const IMPORTANCE_LABELS = ['', 'Unwichtig', 'Wenig wichtig', 'Wichtig', 'Sehr wichtig', 'Entscheidend']
function handleSlider(key: WeightingKey, pct: number) {
onChange({ ...weights, [key]: pct / 100 })
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>
}
function rawToWeights(raw: Record<WeightingKey, number>): Record<WeightingKey, number> {
const total = WEIGHTING_KEYS.reduce((s, k) => s + (raw[k] ?? 1), 0)
return Object.fromEntries(
WEIGHTING_KEYS.map(k => [k, (raw[k] ?? 1) / total])
) as Record<WeightingKey, number>
}
export function WeightingEditor({ weights, onChange, assetType }: Props) {
const [raw, setRaw] = useState<Record<WeightingKey, number>>(() => toRaw(weights))
function handleSlider(key: WeightingKey, value: number) {
const updated = { ...raw, [key]: value }
setRaw(updated)
onChange(rawToWeights(updated))
}
function handleReset() {
onChange(weightingService.getDefaultWeights(assetType))
const defaults = weightingService.getDefaultWeights(assetType)
setRaw(toRaw(defaults))
onChange(defaults)
}
return (
@@ -28,10 +48,10 @@ export function WeightingEditor({ weights, onChange, assetType }: Props) {
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 2 }}>
<Box>
<Typography variant="subtitle1" sx={{ fontWeight: 600 }}>
Kriteriengewichtung anpassen
Wichtigkeit der Kriterien
</Typography>
<Typography variant="caption" color="text.secondary">
Passen Sie an, wie stark jedes Kriterium das Matching beeinflusst.
Schieber nach rechts = wichtiger. Gewichtung wird automatisch berechnet.
</Typography>
</Box>
<Button
@@ -45,24 +65,25 @@ export function WeightingEditor({ weights, onChange, assetType }: Props) {
</Box>
<Card sx={{ p: 3 }}>
<Box sx={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 3 }}>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2.5 }}>
{WEIGHTING_KEYS.map(key => {
const pct = Math.round((weights[key] ?? 0) * 100)
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="body2" color="text.secondary">
{pct}%
<Typography variant="caption" color="text.secondary">
{IMPORTANCE_LABELS[importance]}
</Typography>
</Box>
<Slider
value={pct}
min={0}
max={40}
value={importance}
min={1}
max={5}
step={1}
marks
onChange={(_, v) => handleSlider(key, v as number)}
size="small"
sx={{ color: '#1e3a5f' }}
@@ -71,27 +92,6 @@ export function WeightingEditor({ weights, onChange, assetType }: Props) {
)
})}
</Box>
<Box
sx={{
mt: 3,
pt: 2,
borderTop: '1px solid #e2e8f0',
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
}}
>
<Typography variant="body2" color="text.secondary">
Gesamt
</Typography>
<Typography
variant="body2"
sx={{ fontWeight: 700, color: isBalanced ? '#1a7a4a' : '#c0392b' }}
>
{totalPct}%{!isBalanced && ' — Summe sollte ~100% ergeben'}
</Typography>
</Box>
</Card>
</Box>
)