import { memo, useMemo, useState } from 'react' import { Accordion, AccordionDetails, AccordionSummary, Box, Button, Chip, Divider, LinearProgress, Slider, Switch, TextField, Typography, } from '@mui/material' import { Bell, Calendar, CheckCircle2, ChevronDown, MapPin, Ruler, Target, Wallet } from 'lucide-react' import type { Need, NotificationConfig, UpdateNeedInput } from '../../domain/need' import type { Property } from '../../domain/property' import { ASSET_TYPE_LABELS } from '../../lib/constants' import { confidenceHex } from '../../lib/utils' import { useMatchesByNeed } from '../../hooks/useMatches' import { PropertyMatchRow } from './PropertyMatchRow' const WEIGHT_LABELS: Record = { area: 'Fläche', location: 'Standort', budget: 'Budget', timing: 'Timing', prestige: 'Prestige', accessibility: 'Erreichbarkeit', expansionPotential: 'Expansionspotenzial', flexibility: 'Flexibilität', visibility: 'Sichtbarkeit', footfall: 'Publikumsverkehr', talentAccess: 'Talentzugang', esg: 'ESG', taxEnvironment: 'Steuerumgebung', } function statusConfig(status: Need['status']): { label: string; bg: string; fg: string } { if (status === 'DRAFT') return { label: 'Entwurf', bg: '#fef3c7', fg: '#92400e' } return { label: 'Aktiv', bg: '#dcfce7', fg: '#166534' } } interface Props { need: Need properties: Property[] onStart: () => void onEdit: () => void onArchive: () => void onUpdate: (data: UpdateNeedInput) => void } export const SavedNeedDetail = memo(function SavedNeedDetail({ need, properties, onStart, onEdit, onArchive, onUpdate }: Props) { const statusCfg = statusConfig(need.status) const conf = need.confidenceInCriteria ?? 0 const confColor = confidenceHex(conf) const [notif, setNotif] = useState(() => need.notificationConfig ?? { enabled: true, minScore: 80 }) const [notifDirty, setNotifDirty] = useState(false) function handleNotifChange(patch: Partial) { setNotif(prev => ({ ...prev, ...patch })) setNotifDirty(true) } function handleNotifSave() { onUpdate({ notificationConfig: notif }) setNotifDirty(false) } const { data: matches = [] } = useMatchesByNeed(need.id) const notifThreshold = need.notificationConfig?.minScore ?? 80 const matchCount = matches.filter(m => m.matchScore >= notifThreshold).length const scoredProperties = useMemo(() => { const seen = new Set() return matches .map(m => { const property = properties.find(p => p.id === m.propertyId) return property ? { property, score: Math.round(m.matchScore) } : null }) .filter((x): x is { property: Property; score: number } => x !== null) .sort((a, b) => b.score - a.score) .filter(x => { if (seen.has(x.property.id)) return false seen.add(x.property.id) return true }) }, [matches, properties]) const topScore = scoredProperties.length > 0 ? scoredProperties[0].score : 0 const topProperties = scoredProperties.slice(0, 3) const topWeights = useMemo( () => Object.entries(need.weightingProfile).filter(([, v]) => v > 0.03).sort(([, a], [, b]) => b - a).slice(0, 6), [need.weightingProfile], ) return ( {need.companyName} {need.contactName && ( {need.contactName} )} Suchkriterien } label="Standort" value={need.preferredLocations.join(', ') || '—'} /> } label="Fläche" value={`${need.requiredArea.min}–${need.requiredArea.max} m²`} /> } label="Budget" value={`CHF ${need.budgetRange.maxPerSqm} /m²`} /> } label="Einzug ab" value={need.timing.earliestMoveIn || '—'} /> Konfidenz der Kriterien {Math.round(conf * 100)}% {(need.mustCriteriaText?.length ?? 0) > 0 && ( Must-have Kriterien {need.mustCriteriaText!.map((c, i) => ( {c} ))} )} {topWeights.length > 0 && ( } sx={{ px: 2, minHeight: 44, '& .MuiAccordionSummary-content': { my: 0 } }}> Gewichtete Präferenzen {topWeights.map(([key, val]) => { const pct = Math.round(val * 100) return ( {WEIGHT_LABELS[key] ?? key} {pct}% ) })} )} } sx={{ px: 2, minHeight: 44, '& .MuiAccordionSummary-content': { my: 0 } }}> Benachrichtigungen {notif.enabled && ( ab {notif.minScore}% )} Benachrichtigung aktiv Neue Treffer oberhalb der Schwelle melden handleNotifChange({ enabled: e.target.checked })} sx={{ '& .MuiSwitch-thumb': { bgcolor: notif.enabled ? '#152642' : undefined } }} /> {notif.enabled && ( <> Match-Schwelle {notif.minScore}% handleNotifChange({ minScore: v as number })} sx={{ color: '#152642', '& .MuiSlider-thumb': { width: 14, height: 14 } }} /> 60% 95% E-Mail (optional) handleNotifChange({ emailAddress: e.target.value || undefined })} inputProps={{ type: 'email' }} sx={{ '& .MuiOutlinedInput-root': { fontSize: '0.85rem' } }} helperText="Im Produktivsystem wird bei neuen Treffern eine E-Mail ausgelöst" /> )} {notifDirty && ( )} {scoredProperties.length > 0 && ( <> Passende Objekte {matchCount} Treffer {topProperties.map(({ property, score }) => ( ))} {matchCount > 3 && ( Alle {matchCount} Ergebnisse anzeigen → )} )} ) }) function SectionLabel({ children }: { children: React.ReactNode }) { return ( {children} ) } function CriteriaBox({ icon, label, value }: { icon: React.ReactNode; label: string; value: string }) { return ( {icon} {label} {value} ) }