From ee71ecc881508d5d55881bf43a36cae89af05726 Mon Sep 17 00:00:00 2001 From: Benjamin Sutter Date: Sun, 24 May 2026 01:08:45 +0200 Subject: [PATCH] refactor: extract sub-components from PropertyMarketSignalsTab + NegotiationInsightsPanel MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - PropertyMarketSignalsTab (416→100 lines): SignalCard, BerichtDialog → own files - NegotiationInsightsPanel (345→260 lines): pure logic → negotiationInsightsUtils.ts Co-Authored-By: Claude Sonnet 4.6 --- src/components/supply/BerichtDialog.tsx | 238 +++++++++++++ .../supply/NegotiationInsightsPanel.tsx | 87 +---- .../supply/PropertyMarketSignalsTab.tsx | 326 +----------------- src/components/supply/SignalCard.tsx | 81 +++++ .../supply/negotiationInsightsUtils.ts | 88 +++++ 5 files changed, 414 insertions(+), 406 deletions(-) create mode 100644 src/components/supply/BerichtDialog.tsx create mode 100644 src/components/supply/SignalCard.tsx create mode 100644 src/components/supply/negotiationInsightsUtils.ts diff --git a/src/components/supply/BerichtDialog.tsx b/src/components/supply/BerichtDialog.tsx new file mode 100644 index 0000000..11a7dd7 --- /dev/null +++ b/src/components/supply/BerichtDialog.tsx @@ -0,0 +1,238 @@ +import { useEffect, useRef, useState } from 'react' +import { Box, Button, CircularProgress, Dialog, LinearProgress, Typography } from '@mui/material' +import { CheckCircle, Download, FileText } from 'lucide-react' +import type { MarketReport } from '../../domain/marketReport' + +const SECTION_COLORS: Record = { + development: '#1e3a5f', + demand: '#1a7a4a', + supply: '#b45309', + negotiation: '#7c3aed', +} +const SECTION_LABELS: Record = { + development: 'Entwicklungsnews', + demand: 'Nachfrage 5 km', + supply: 'Angebot 5 km', + negotiation: 'Verhandlungshinweise', +} + +interface BerichtDialogProps { + onClose: () => void + report: MarketReport | null + propertyId: string +} + +export function BerichtDialog({ onClose, report, propertyId }: BerichtDialogProps) { + const [ready, setReady] = useState(false) + const [progress, setProgress] = useState(0) + const timerRef = useRef | null>(null) + + useEffect(() => { + let p = 0 + timerRef.current = setInterval(() => { + p += Math.random() * 18 + 8 + if (p >= 100) { + clearInterval(timerRef.current!) + setProgress(100) + setReady(true) + } else { + setProgress(Math.min(100, p)) + } + }, 200) + return () => { if (timerRef.current) clearInterval(timerRef.current) } + }, []) + + function handleDownload() { + const signals = report?.signals ?? [] + const lines: string[] = [ + 'MARKTSIGNAL-BERICHT', + '====================', + `Objekt-ID: ${propertyId}`, + `Erstellt: ${new Date().toLocaleString('de-CH')}`, + '', + ] + for (const s of signals) { + lines.push(`[${SECTION_LABELS[s.category] ?? s.category}]`) + lines.push(s.title) + lines.push(s.body) + if (s.source) lines.push(`Quelle: ${s.source}`) + lines.push('') + } + const blob = new Blob([lines.join('\n')], { type: 'application/pdf' }) + const url = URL.createObjectURL(blob) + const a = document.createElement('a') + a.href = url + a.download = `Marktsignal-Bericht_${propertyId}.pdf` + a.click() + URL.revokeObjectURL(url) + onClose() + } + + const signals = report?.signals ?? [] + const categories = ['development', 'demand', 'supply', 'negotiation'] as const + + return ( + + {/* Header */} + + + + Bericht erstellen + + + Marktsignal-Bericht · {propertyId} + + + {ready && ( + + + Bereit + + )} + + + {/* Generating state */} + {!ready && ( + + + + + PDF-Bericht wird generiert… + + {Math.round(progress)}% + + + + )} + + {/* PDF Preview */} + + + {/* Document header */} + + + + + Marktsignal-Bericht + + + + Wincasa AG · Zürich + {new Date().toLocaleDateString('de-CH')} + + + + + + + Standortintelligenz & Marktsignale + + + Objekt-ID: {propertyId} · Generiert: {report?.generatedAt ? new Date(report.generatedAt).toLocaleDateString('de-CH') : new Date().toLocaleDateString('de-CH')} · {signals.length} Signale + + + {/* Sections */} + {categories.map(cat => { + const catSignals = signals.filter(s => s.category === cat) + if (catSignals.length === 0) return null + const color = SECTION_COLORS[cat] + const label = SECTION_LABELS[cat] + return ( + + + + + {label} + + + ({catSignals.length}) + + + {catSignals.map((s, i) => ( + + + + {s.title} + + {s.confidence != null && ( + + KI-Konfidenz: {Math.round(s.confidence * 100)}% + + )} + + + {s.body} + + + {s.date && ( + + {new Date(s.date).toLocaleDateString('de-CH')} + + )} + {s.source && ( + + Quelle: {s.source} + + )} + + + ))} + + ) + })} + + {signals.length === 0 && ( + + Keine Marktsignale für dieses Objekt verfügbar. + + )} + + {/* Footer */} + + + Dieser Bericht wurde automatisch auf Basis von KI-generierten Marktsignalen erstellt. · Wincasa AG + + + + + + {/* Footer actions */} + + + + + + ) +} diff --git a/src/components/supply/NegotiationInsightsPanel.tsx b/src/components/supply/NegotiationInsightsPanel.tsx index c8d9a06..baae88e 100644 --- a/src/components/supply/NegotiationInsightsPanel.tsx +++ b/src/components/supply/NegotiationInsightsPanel.tsx @@ -4,92 +4,7 @@ import { useProperties } from '../../hooks/useProperties' import { useNeeds } from '../../hooks/useNeeds' import { getCityIntelligence, getMarketRent } from '../../lib/locationIntelligence' import type { Property } from '../../domain/property' - -// ── Selling argument generator ──────────────────────────────────────────────── - -interface Argument { - title: string - detail: string - strength: 'strong' | 'medium' -} - -function generateSellingArguments(p: Property): Argument[] { - const args: Argument[] = [] - const sf = p.softFactors - const intel = getCityIntelligence(p.location.city) - - if (sf) { - const footfall = sf.footfallScore ?? (sf.passerbyFrequency ? { LOW: 0.25, MEDIUM: 0.5, HIGH: 0.78, VERY_HIGH: 0.95 }[sf.passerbyFrequency] ?? 0 : 0) - if (footfall >= 0.72) - args.push({ title: 'Hervorragende Frequenzlage', detail: 'Überdurchschnittliche Passantenfrequenz — sichert Sichtbarkeit und Kundenzugang.', strength: 'strong' }) - - const taxScore = sf.taxEnvironmentScore ?? (intel ? Math.max(0, 1 - intel.taxIndexCanton / 150) : undefined) - if (taxScore !== undefined && taxScore >= 0.65) - args.push({ title: 'Steuerattraktiver Standort', detail: `${p.location.city} bietet eine günstige Steuerlast${intel ? ` (Index ${intel.taxIndexCanton}, CH = 100)` : ''} — relevant für Unternehmensansiedlungen.`, strength: 'strong' }) - - const ov = sf.commuterAccessScore - if (ov !== undefined && ov >= 0.72) - args.push({ title: 'Sehr gute ÖV-Anbindung', detail: sf.publicTransportMinutes ? `Ca. ${sf.publicTransportMinutes} Min. zum nächsten Bahnhof.` : 'Ausgezeichnete öffentliche Erreichbarkeit.', strength: 'strong' }) - - const prestige = sf.prestigeScore ?? (typeof sf.prestige === 'number' ? sf.prestige : undefined) - if (prestige !== undefined && prestige >= 0.7) - args.push({ title: 'Repräsentativer Standort', detail: 'Hoher Prestige-Wert — ideal für Unternehmen mit Repräsentationsanspruch und Aussenauftritt.', strength: 'strong' }) - - const talent = sf.talentAccessScore ?? (typeof sf.talentAccess === 'number' ? sf.talentAccess : undefined) - if (talent !== undefined && talent >= 0.65) - args.push({ title: 'Grosser Talentpool', detail: 'Zugang zu gut ausgebildeten Fachkräften im Einzugsgebiet — entscheidend für wachsende Unternehmen.', strength: 'medium' }) - - if (sf.flexibilityScore !== undefined && sf.flexibilityScore >= 0.65) - args.push({ title: 'Flexible Flächengestaltung', detail: 'Grundriss und Ausbaustandard ermöglichen individuelle Anpassungen.', strength: 'medium' }) - - if (sf.esgScore !== undefined && sf.esgScore >= 0.7) - args.push({ title: 'Nachhaltigkeitszertifizierung', detail: 'Guter ESG-Score — relevant für Unternehmen mit Nachhaltigkeitszielen und ESG-Reporting.', strength: 'medium' }) - } - - if (p.hardFacts?.isBarrierFree) - args.push({ title: 'Barrierefrei', detail: 'Vollständig rollstuhlgängig — gesetzlich zunehmend gefordert.', strength: 'medium' }) - - if (p.hardFacts?.parking && p.hardFacts.parking > 0) - args.push({ title: `${p.hardFacts.parking} Parkplätze inkl.`, detail: 'Eigene Parkierungsmöglichkeiten — in Städten ein knappes Gut.', strength: 'medium' }) - - if (p.hardFacts?.hasServerRoom) - args.push({ title: 'Serverraum vorhanden', detail: 'Sofortig nutzbare IT-Infrastruktur — spart Einrichtungskosten.', strength: 'medium' }) - - if (intel?.demandStrength === 'VERY_HIGH' || intel?.demandStrength === 'HIGH') - args.push({ title: 'Stark nachgefragter Markt', detail: `${p.location.city} verzeichnet ${intel.demandStrength === 'VERY_HIGH' ? 'sehr hohe' : 'hohe'} Nachfrage — kurze Leerstandszeiten zu erwarten.`, strength: 'strong' }) - - return args -} - -// ── Proactive weakness acknowledgement ─────────────────────────────────────── - -interface Weakness { - issue: string - mitigation: string -} - -function generateWeaknesses(p: Property): Weakness[] { - const ws: Weakness[] = [] - const sf = p.softFactors - const intel = getCityIntelligence(p.location.city) - - if (intel && intel.vacancyRatePct >= 5) - ws.push({ issue: 'Hohe Leerstandsquote in der Region', mitigation: 'Mietfreie Zeit oder Ausbaukostenbeteiligung als Anreiz anbieten.' }) - - if (intel && intel.taxIndexCanton >= 115) - ws.push({ issue: 'Überdurchschnittliche Steuerlast', mitigation: 'Andere Standortvorteile (Prestige, ÖV) gezielt hervorheben.' }) - - if (sf?.commuterAccessScore !== undefined && sf.commuterAccessScore < 0.45) - ws.push({ issue: 'Eingeschränkte ÖV-Anbindung', mitigation: 'Parkplatz-Angebot und Veloinfrastruktur als Alternative betonen.' }) - - if (p.hardFacts?.parking === 0 || (p.hardFacts?.parking === undefined && !sf?.parkingSpots)) - ws.push({ issue: 'Keine eigenen Parkplätze', mitigation: 'Öffentliche Parkhäuser in der Nähe aufzeigen. Ggf. Parkabonnement als Mietbonus anbieten.' }) - - if (p.dataQuality.score < 0.65) - ws.push({ issue: 'Unvollständige Objektdaten', mitigation: 'Fehlende Angaben vor dem Gespräch vervollständigen, um Vertrauen zu stärken.' }) - - return ws -} +import { generateSellingArguments, generateWeaknesses } from './negotiationInsightsUtils' // ── Main component ──────────────────────────────────────────────────────────── diff --git a/src/components/supply/PropertyMarketSignalsTab.tsx b/src/components/supply/PropertyMarketSignalsTab.tsx index f5903aa..1eab4df 100644 --- a/src/components/supply/PropertyMarketSignalsTab.tsx +++ b/src/components/supply/PropertyMarketSignalsTab.tsx @@ -1,8 +1,10 @@ -import { useEffect, useRef, useState } from 'react' -import { Alert, Box, Button, Chip, CircularProgress, Dialog, DialogContent, DialogTitle, LinearProgress, Typography } from '@mui/material' -import { BarChart2, Building2, CheckCircle, Download, FileText, Lightbulb, TrendingUp } from 'lucide-react' +import { useEffect, useState } from 'react' +import { Alert, Box, Button, Chip, CircularProgress, Typography } from '@mui/material' +import { BarChart2, Building2, FileText, Lightbulb, TrendingUp } from 'lucide-react' import { marketReportService } from '../../services/marketReportService' -import type { MarketReport, PropertyMarketSignal, SignalCategory } from '../../domain/marketReport' +import type { MarketReport, SignalCategory } from '../../domain/marketReport' +import { SignalCard } from './SignalCard' +import { BerichtDialog } from './BerichtDialog' interface Props { propertyId: string @@ -15,322 +17,6 @@ const SECTION_CONFIG: { category: SignalCategory; label: string; icon: React.Rea { category: 'negotiation', label: 'Verhandlungshinweise', icon: , color: '#7c3aed' }, ] -function SignalCard({ signal }: { signal: PropertyMarketSignal }) { - return ( - - - - {signal.title} - - {signal.confidence != null && ( - - )} - - - - {signal.body} - - - {signal.confidence != null && ( - - - KI-Konfidenz - {Math.round(signal.confidence * 100)}% - - = 0.8 ? '#1a7a4a' : signal.confidence >= 0.6 ? '#d97706' : '#64748b' }, - }} - /> - - )} - - - {signal.date && ( - - {new Date(signal.date).toLocaleDateString('de-CH')} - - )} - {signal.source && ( - - - {signal.source} - - )} - - - ) -} - -// ── PDF preview dialog ──────────────────────────────────────────────────────── - -const SECTION_COLORS: Record = { - development: '#1e3a5f', - demand: '#1a7a4a', - supply: '#b45309', - negotiation: '#7c3aed', -} -const SECTION_LABELS: Record = { - development: 'Entwicklungsnews', - demand: 'Nachfrage 5 km', - supply: 'Angebot 5 km', - negotiation: 'Verhandlungshinweise', -} - -interface BerichtDialogProps { - onClose: () => void - report: MarketReport | null - propertyId: string -} - -function BerichtDialog({ onClose, report, propertyId }: BerichtDialogProps) { - const [ready, setReady] = useState(false) - const [progress, setProgress] = useState(0) - const timerRef = useRef | null>(null) - - useEffect(() => { - let p = 0 - timerRef.current = setInterval(() => { - p += Math.random() * 18 + 8 - if (p >= 100) { - clearInterval(timerRef.current!) - setProgress(100) - setReady(true) - } else { - setProgress(Math.min(100, p)) - } - }, 200) - return () => { if (timerRef.current) clearInterval(timerRef.current) } - }, []) - - function handleDownload() { - const signals = report?.signals ?? [] - const lines: string[] = [ - 'MARKTSIGNAL-BERICHT', - '====================', - `Objekt-ID: ${propertyId}`, - `Erstellt: ${new Date().toLocaleString('de-CH')}`, - '', - ] - for (const s of signals) { - lines.push(`[${SECTION_LABELS[s.category] ?? s.category}]`) - lines.push(s.title) - lines.push(s.body) - if (s.source) lines.push(`Quelle: ${s.source}`) - lines.push('') - } - const blob = new Blob([lines.join('\n')], { type: 'application/pdf' }) - const url = URL.createObjectURL(blob) - const a = document.createElement('a') - a.href = url - a.download = `Marktsignal-Bericht_${propertyId}.pdf` - a.click() - URL.revokeObjectURL(url) - onClose() - } - - const signals = report?.signals ?? [] - const categories = ['development', 'demand', 'supply', 'negotiation'] as const - - return ( - - {/* Header */} - - - - Bericht erstellen - - - Marktsignal-Bericht · {propertyId} - - - {ready && ( - - - Bereit - - )} - - - {/* Generating state */} - {!ready && ( - - - - - PDF-Bericht wird generiert… - - {Math.round(progress)}% - - - - )} - - {/* PDF Preview */} - - - {/* Document header */} - - - - - Marktsignal-Bericht - - - - Wincasa AG · Zürich - {new Date().toLocaleDateString('de-CH')} - - - - - - - Standortintelligenz & Marktsignale - - - Objekt-ID: {propertyId} · Generiert: {report?.generatedAt ? new Date(report.generatedAt).toLocaleDateString('de-CH') : new Date().toLocaleDateString('de-CH')} · {signals.length} Signale - - - {/* Sections */} - {categories.map(cat => { - const catSignals = signals.filter(s => s.category === cat) - if (catSignals.length === 0) return null - const color = SECTION_COLORS[cat] - const label = SECTION_LABELS[cat] - return ( - - - - - {label} - - - ({catSignals.length}) - - - {catSignals.map((s, i) => ( - - - - {s.title} - - {s.confidence != null && ( - - KI-Konfidenz: {Math.round(s.confidence * 100)}% - - )} - - - {s.body} - - - {s.date && ( - - {new Date(s.date).toLocaleDateString('de-CH')} - - )} - {s.source && ( - - Quelle: {s.source} - - )} - - - ))} - - ) - })} - - {signals.length === 0 && ( - - Keine Marktsignale für dieses Objekt verfügbar. - - )} - - {/* Footer */} - - - Dieser Bericht wurde automatisch auf Basis von KI-generierten Marktsignalen erstellt. · Wincasa AG - - - - - - {/* Footer actions */} - - - - - - ) -} - -// ── Main tab component ──────────────────────────────────────────────────────── - export function PropertyMarketSignalsTab({ propertyId }: Props) { const [report, setReport] = useState(null) const [loading, setLoading] = useState(true) diff --git a/src/components/supply/SignalCard.tsx b/src/components/supply/SignalCard.tsx new file mode 100644 index 0000000..17112e2 --- /dev/null +++ b/src/components/supply/SignalCard.tsx @@ -0,0 +1,81 @@ +import { Box, Chip, LinearProgress, Typography } from '@mui/material' +import { FileText } from 'lucide-react' +import type { PropertyMarketSignal } from '../../domain/marketReport' + +export function SignalCard({ signal }: { signal: PropertyMarketSignal }) { + return ( + + + + {signal.title} + + {signal.confidence != null && ( + + )} + + + + {signal.body} + + + {signal.confidence != null && ( + + + KI-Konfidenz + {Math.round(signal.confidence * 100)}% + + = 0.8 ? '#1a7a4a' : signal.confidence >= 0.6 ? '#d97706' : '#64748b' }, + }} + /> + + )} + + + {signal.date && ( + + {new Date(signal.date).toLocaleDateString('de-CH')} + + )} + {signal.source && ( + + + {signal.source} + + )} + + + ) +} diff --git a/src/components/supply/negotiationInsightsUtils.ts b/src/components/supply/negotiationInsightsUtils.ts new file mode 100644 index 0000000..c695a08 --- /dev/null +++ b/src/components/supply/negotiationInsightsUtils.ts @@ -0,0 +1,88 @@ +import { getCityIntelligence } from '../../lib/locationIntelligence' +import type { Property } from '../../domain/property' + +// ── Selling argument generator ──────────────────────────────────────────────── + +export interface Argument { + title: string + detail: string + strength: 'strong' | 'medium' +} + +export function generateSellingArguments(p: Property): Argument[] { + const args: Argument[] = [] + const sf = p.softFactors + const intel = getCityIntelligence(p.location.city) + + if (sf) { + const footfall = sf.footfallScore ?? (sf.passerbyFrequency ? { LOW: 0.25, MEDIUM: 0.5, HIGH: 0.78, VERY_HIGH: 0.95 }[sf.passerbyFrequency] ?? 0 : 0) + if (footfall >= 0.72) + args.push({ title: 'Hervorragende Frequenzlage', detail: 'Überdurchschnittliche Passantenfrequenz — sichert Sichtbarkeit und Kundenzugang.', strength: 'strong' }) + + const taxScore = sf.taxEnvironmentScore ?? (intel ? Math.max(0, 1 - intel.taxIndexCanton / 150) : undefined) + if (taxScore !== undefined && taxScore >= 0.65) + args.push({ title: 'Steuerattraktiver Standort', detail: `${p.location.city} bietet eine günstige Steuerlast${intel ? ` (Index ${intel.taxIndexCanton}, CH = 100)` : ''} — relevant für Unternehmensansiedlungen.`, strength: 'strong' }) + + const ov = sf.commuterAccessScore + if (ov !== undefined && ov >= 0.72) + args.push({ title: 'Sehr gute ÖV-Anbindung', detail: sf.publicTransportMinutes ? `Ca. ${sf.publicTransportMinutes} Min. zum nächsten Bahnhof.` : 'Ausgezeichnete öffentliche Erreichbarkeit.', strength: 'strong' }) + + const prestige = sf.prestigeScore ?? (typeof sf.prestige === 'number' ? sf.prestige : undefined) + if (prestige !== undefined && prestige >= 0.7) + args.push({ title: 'Repräsentativer Standort', detail: 'Hoher Prestige-Wert — ideal für Unternehmen mit Repräsentationsanspruch und Aussenauftritt.', strength: 'strong' }) + + const talent = sf.talentAccessScore ?? (typeof sf.talentAccess === 'number' ? sf.talentAccess : undefined) + if (talent !== undefined && talent >= 0.65) + args.push({ title: 'Grosser Talentpool', detail: 'Zugang zu gut ausgebildeten Fachkräften im Einzugsgebiet — entscheidend für wachsende Unternehmen.', strength: 'medium' }) + + if (sf.flexibilityScore !== undefined && sf.flexibilityScore >= 0.65) + args.push({ title: 'Flexible Flächengestaltung', detail: 'Grundriss und Ausbaustandard ermöglichen individuelle Anpassungen.', strength: 'medium' }) + + if (sf.esgScore !== undefined && sf.esgScore >= 0.7) + args.push({ title: 'Nachhaltigkeitszertifizierung', detail: 'Guter ESG-Score — relevant für Unternehmen mit Nachhaltigkeitszielen und ESG-Reporting.', strength: 'medium' }) + } + + if (p.hardFacts?.isBarrierFree) + args.push({ title: 'Barrierefrei', detail: 'Vollständig rollstuhlgängig — gesetzlich zunehmend gefordert.', strength: 'medium' }) + + if (p.hardFacts?.parking && p.hardFacts.parking > 0) + args.push({ title: `${p.hardFacts.parking} Parkplätze inkl.`, detail: 'Eigene Parkierungsmöglichkeiten — in Städten ein knappes Gut.', strength: 'medium' }) + + if (p.hardFacts?.hasServerRoom) + args.push({ title: 'Serverraum vorhanden', detail: 'Sofortig nutzbare IT-Infrastruktur — spart Einrichtungskosten.', strength: 'medium' }) + + if (intel?.demandStrength === 'VERY_HIGH' || intel?.demandStrength === 'HIGH') + args.push({ title: 'Stark nachgefragter Markt', detail: `${p.location.city} verzeichnet ${intel.demandStrength === 'VERY_HIGH' ? 'sehr hohe' : 'hohe'} Nachfrage — kurze Leerstandszeiten zu erwarten.`, strength: 'strong' }) + + return args +} + +// ── Proactive weakness acknowledgement ─────────────────────────────────────── + +export interface Weakness { + issue: string + mitigation: string +} + +export function generateWeaknesses(p: Property): Weakness[] { + const ws: Weakness[] = [] + const sf = p.softFactors + const intel = getCityIntelligence(p.location.city) + + if (intel && intel.vacancyRatePct >= 5) + ws.push({ issue: 'Hohe Leerstandsquote in der Region', mitigation: 'Mietfreie Zeit oder Ausbaukostenbeteiligung als Anreiz anbieten.' }) + + if (intel && intel.taxIndexCanton >= 115) + ws.push({ issue: 'Überdurchschnittliche Steuerlast', mitigation: 'Andere Standortvorteile (Prestige, ÖV) gezielt hervorheben.' }) + + if (sf?.commuterAccessScore !== undefined && sf.commuterAccessScore < 0.45) + ws.push({ issue: 'Eingeschränkte ÖV-Anbindung', mitigation: 'Parkplatz-Angebot und Veloinfrastruktur als Alternative betonen.' }) + + if (p.hardFacts?.parking === 0 || (p.hardFacts?.parking === undefined && !sf?.parkingSpots)) + ws.push({ issue: 'Keine eigenen Parkplätze', mitigation: 'Öffentliche Parkhäuser in der Nähe aufzeigen. Ggf. Parkabonnement als Mietbonus anbieten.' }) + + if (p.dataQuality.score < 0.65) + ws.push({ issue: 'Unvollständige Objektdaten', mitigation: 'Fehlende Angaben vor dem Gespräch vervollständigen, um Vertrauen zu stärken.' }) + + return ws +}