Files
property-match/.claude/worktrees/agent-a82a3716/src/components/demand/NeedCardPreview.tsx
T
Benjamin Sutter d15a13e485 feat: remove Administration workspace — keep only Verwaltung + Suche
- Delete all ops page components (ReviewQueue, AIMonitoring, Governance,
  SourceMonitoring, ActivityTimeline, SignalPipeline)
- Remove OPERATIONS workspace from AppShell config, nav order, path detection
- Remove all /ops/* routes from App.tsx
- Remove WorkspaceType.OPERATIONS from allowedWorkspaces in authService,
  sessionStore, permissions
- Keep MarketIntelligence page (already moved to /supply/market-intelligence)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 20:32:41 +02:00

185 lines
7.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { Box, Card, Chip, Divider, LinearProgress, Stack, TextField, Typography, Alert } from '@mui/material'
import { MapPin, Ruler, Wallet, Clock, CheckSquare, ShieldAlert } from 'lucide-react'
import type { ParsedNeedCriteria } from '../../domain/needBuilder'
import { WEIGHTING_KEYS, WEIGHTING_LABELS } from '../../domain/needBuilder'
import type { WeightingKey } from '../../domain/needBuilder'
interface Props {
criteria: ParsedNeedCriteria
weights: Record<WeightingKey, number>
confidenceByField: Record<string, number>
missingFields: string[]
needTitle: string
onNeedTitleChange: (v: string) => void
}
const ASSET_LABELS: Record<string, string> = {
OFFICE: 'Büro', LOGISTICS: 'Logistik / Lager', RETAIL: 'Retail',
PRODUCTION: 'Produktion', GASTRO: 'Gastro / F&B', LIGHT_INDUSTRIAL: 'Light Industrial',
}
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
return (
<Box sx={{ maxWidth: 720, mx: 'auto' }}>
<Typography variant="subtitle1" sx={{ fontWeight: 600, mb: 2 }}>
Vorschau Neuer Bedarf
</Typography>
{/* Need Title */}
<TextField
fullWidth
size="small"
label="Bedarf Bezeichnung"
placeholder="z.B. Bürofläche Zürich Q4/2025"
value={needTitle}
onChange={e => onNeedTitleChange(e.target.value)}
sx={{ mb: 2 }}
/>
{/* Low-confidence warning */}
{isLowConfidence && (
<Alert severity="warning" icon={<ShieldAlert size={18} />} sx={{ mb: 2 }}>
Gesamtkonfidenz niedrig ({Math.round(overallConfidence * 100)}%) Bedarf wird als Entwurf gespeichert und muss manuell geprüft werden.
</Alert>
)}
{/* Critical missing fields */}
{criticalMissing.length > 0 && (
<Alert severity="error" sx={{ mb: 2 }}>
Fehlende Pflichtfelder: {criticalMissing.join(', ')}. Bitte in den Kriterien ergänzen.
</Alert>
)}
<Card sx={{ p: 3, mb: 2 }}>
{/* Header */}
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 2 }}>
{c.assetType && (
<Chip label={ASSET_LABELS[c.assetType] ?? c.assetType} size="small" sx={{ bgcolor: '#1e3a5f', color: 'white' }} />
)}
<Chip label={isLowConfidence ? 'ENTWURF (needs_review)' : 'ENTWURF'} size="small" variant="outlined" />
</Box>
<Box sx={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 2, mb: 2 }}>
{c.preferredLocations && c.preferredLocations.length > 0 && (
<Box sx={{ display: 'flex', gap: 1 }}>
<MapPin size={16} color="#64748b" style={{ flexShrink: 0, marginTop: 2 }} />
<Box>
<Typography variant="caption" color="text.secondary">Standort</Typography>
<Typography variant="body2">{c.preferredLocations.join(', ')}</Typography>
</Box>
</Box>
)}
{c.areaRange && (
<Box sx={{ display: 'flex', gap: 1 }}>
<Ruler size={16} color="#64748b" style={{ flexShrink: 0, marginTop: 2 }} />
<Box>
<Typography variant="caption" color="text.secondary">Fläche</Typography>
<Typography variant="body2">{c.areaRange.min}{c.areaRange.max} m²</Typography>
</Box>
</Box>
)}
{c.budgetRange && (
<Box sx={{ display: 'flex', gap: 1 }}>
<Wallet size={16} color="#64748b" style={{ flexShrink: 0, marginTop: 2 }} />
<Box>
<Typography variant="caption" color="text.secondary">Budget</Typography>
<Typography variant="body2">max. CHF {c.budgetRange.maxPerSqm}/m²</Typography>
</Box>
</Box>
)}
{c.timing && (
<Box sx={{ display: 'flex', gap: 1 }}>
<Clock size={16} color="#64748b" style={{ flexShrink: 0, marginTop: 2 }} />
<Box>
<Typography variant="caption" color="text.secondary">Verfügbarkeit</Typography>
<Typography variant="body2">ab {c.timing.earliestMoveIn}</Typography>
</Box>
</Box>
)}
</Box>
{c.mustHaveCriteria && c.mustHaveCriteria.length > 0 && (
<Box sx={{ mb: 2 }}>
<Box sx={{ display: 'flex', gap: 1, mb: 0.5 }}>
<CheckSquare size={16} color="#64748b" />
<Typography variant="caption" color="text.secondary">Must-haves</Typography>
</Box>
<Stack direction="row" spacing={0.5} sx={{ flexWrap: 'wrap', gap: 0.5 }}>
{c.mustHaveCriteria.map(m => (
<Chip key={m} label={m} size="small" variant="outlined" />
))}
</Stack>
</Box>
)}
<Divider sx={{ my: 2 }} />
{/* Confidence Summary */}
<Box sx={{ mb: 2 }}>
<Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 0.5 }}>
<Typography variant="caption" sx={{ fontWeight: 600 }} color="text.secondary">
Gesamtkonfidenz
</Typography>
<Typography
variant="caption"
sx={{ fontWeight: 700, color: overallConfidence >= 0.7 ? '#1a7a4a' : overallConfidence >= 0.5 ? '#d97706' : '#c0392b' }}
>
{Math.round(overallConfidence * 100)}%
</Typography>
</Box>
<LinearProgress
variant="determinate"
value={overallConfidence * 100}
sx={{
height: 6, borderRadius: 3, bgcolor: '#e2e8f0',
'& .MuiLinearProgress-bar': {
bgcolor: overallConfidence >= 0.7 ? '#1a7a4a' : overallConfidence >= 0.5 ? '#d97706' : '#c0392b',
},
}}
/>
{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>
</Card>
</Box>
)
}