@@ -0,0 +1,345 @@
import { Box , Chip , Divider , LinearProgress , Paper , Typography } from '@mui/material'
import { CheckCircle , TrendingDown , TrendingUp } from 'lucide-react'
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
}
// ── Main component ────────────────────────────────────────────────────────────
interface Props {
property : Property
}
export function NegotiationInsightsPanel ( { property } : Props ) {
const { data : allProperties = [ ] } = useProperties ( )
const { data : needs = [ ] } = useNeeds ( )
const intel = getCityIntelligence ( property . location . city )
const marketRent = getMarketRent ( property . location . city , property . assetType )
const priceDiff = marketRent ? ( ( property . rentPricePerSqm - marketRent ) / marketRent ) * 100 : null
// Comparable properties for price positioning
const comparables = allProperties
. filter ( p = > p . id !== property . id && p . assetType === property . assetType && p . location . city === property . location . city )
const avgComparableRent = comparables . length > 0
? comparables . reduce ( ( s , p ) = > s + p . rentPricePerSqm , 0 ) / comparables.length
: null
// Active needs matching this property type/location
const matchingNeeds = needs . filter ( n = >
n . assetType === property . assetType &&
( n . preferredLocations ? . some ( loc = > loc . toLowerCase ( ) . includes ( property . location . city . toLowerCase ( ) ) ) ? ? false )
)
const sellingArgs = generateSellingArguments ( property )
const weaknesses = generateWeaknesses ( property )
const strongArgs = sellingArgs . filter ( a = > a . strength === 'strong' )
const mediumArgs = sellingArgs . filter ( a = > a . strength === 'medium' )
return (
< Box sx = { { display : 'flex' , flexDirection : 'column' , gap : 2 } } >
{ /* ── Price positioning ── */ }
< Paper sx = { { p : 2.5 } } >
< Typography variant = "subtitle2" sx = { { fontWeight : 700 , mb : 1.5 } } > Preispositionierung < / Typography >
< Box sx = { { display : 'flex' , gap : 1 , flexWrap : 'wrap' , mb : 1.5 } } >
< Box sx = { { flex : 1 , p : 1.5 , bgcolor : '#f8fafc' , borderRadius : 1.5 , border : '1px solid #e2e8f0' , minWidth : 120 } } >
< Typography variant = "caption" color = "text.secondary" > Ihr Preis < / Typography >
< Typography variant = "h6" sx = { { fontWeight : 700 , color : '#0f1923' } } >
CHF { property . rentPricePerSqm } / m ²
< / Typography >
< / Box >
{ marketRent && (
< Box sx = { { flex : 1 , p : 1.5 , bgcolor : '#f8fafc' , borderRadius : 1.5 , border : '1px solid #e2e8f0' , minWidth : 120 } } >
< Typography variant = "caption" color = "text.secondary" > Marktmedian { property . location . city } < / Typography >
< Typography variant = "h6" sx = { { fontWeight : 700 , color : '#475569' } } >
CHF { marketRent } / m ²
< / Typography >
< / Box >
) }
{ avgComparableRent && (
< Box sx = { { flex : 1 , p : 1.5 , bgcolor : '#f8fafc' , borderRadius : 1.5 , border : '1px solid #e2e8f0' , minWidth : 120 } } >
< Typography variant = "caption" color = "text.secondary" > Vergleichsangebote Ø < / Typography >
< Typography variant = "h6" sx = { { fontWeight : 700 , color : '#475569' } } >
CHF { Math . round ( avgComparableRent ) } / m ²
< / Typography >
< / Box >
) }
< / Box >
{ priceDiff !== null && (
< Box sx = { { display : 'flex' , alignItems : 'center' , gap : 1 , p : 1.25 , bgcolor : Math.abs ( priceDiff ) < 10 ? '#f0fdf4' : priceDiff > 0 ? '#fff8f0' : '#f0fdf4' , borderRadius : 1.5 } } >
{ priceDiff > 0 ? < TrendingUp size = { 15 } color = "#d97706" / > : < TrendingDown size = { 15 } color = "#1a7a4a" / > }
< Typography variant = "body2" sx = { { color : priceDiff > 15 ? '#92400e' : priceDiff > 0 ? '#d97706' : '#1a7a4a' , fontWeight : 500 } } >
{ priceDiff > 15
? ` Ihr Preis liegt ${ Math . round ( priceDiff ) } % über dem Marktmedian — starke USPs nötig zur Rechtfertigung. `
: priceDiff > 5
? ` Leicht über Marktmedian (+ ${ Math . round ( priceDiff ) } %) — gut durch Qualität begründbar. `
: priceDiff > - 5
? 'Im Marktdurchschnitt — gute Ausgangsposition.'
: ` ${ Math . round ( Math . abs ( priceDiff ) ) } % unter Marktmedian — Preiserhöhung oder schnelle Vermietung möglich. ` }
< / Typography >
< / Box >
) }
{ /* Price bar vs market */ }
{ marketRent && (
< Box sx = { { mt : 1.5 } } >
< Box sx = { { display : 'flex' , justifyContent : 'space-between' , mb : 0.25 } } >
< Typography variant = "caption" color = "text.secondary" > Marktbereich { property . location . city } < / Typography >
< Typography variant = "caption" color = "text.secondary" >
CHF { Math . round ( marketRent * 0.7 ) } – { Math . round ( marketRent * 1.4 ) } / m ²
< / Typography >
< / Box >
< Box sx = { { position : 'relative' , height : 8 , bgcolor : '#e2e8f0' , borderRadius : 4 } } >
< Box sx = { {
position : 'absolute' ,
left : ` ${ Math . min ( 90 , Math . max ( 5 , ( ( property . rentPricePerSqm - marketRent * 0.7 ) / ( marketRent * 0.7 ) ) * 100 ) ) } % ` ,
top : - 2 ,
width : 12 ,
height : 12 ,
borderRadius : '50%' ,
bgcolor : '#1e3a5f' ,
border : '2px solid white' ,
boxShadow : '0 1px 3px rgba(0,0,0,0.2)' ,
} } / >
< / Box >
< / Box >
) }
< / Paper >
{ /* ── Active demand ── */ }
< Paper sx = { { p : 2.5 } } >
< Typography variant = "subtitle2" sx = { { fontWeight : 700 , mb : 0.5 } } > Aktive Nachfrage < / Typography >
< Typography variant = "body2" color = "text.secondary" sx = { { mb : 1.5 } } >
Unternehmen , die aktuell in { property . location . city } suchen
< / Typography >
{ matchingNeeds . length > 0 ? (
< >
< Box sx = { { display : 'flex' , alignItems : 'center' , gap : 1 , mb : 1.5 } } >
< Typography variant = "h4" sx = { { fontWeight : 800 , color : '#1e3a5f' } } > { matchingNeeds . length } < / Typography >
< Typography variant = "body2" color = "text.secondary" > aktive Suchprofile für diesen Typ & Standort < / Typography >
< / Box >
{ matchingNeeds . slice ( 0 , 4 ) . map ( n = > (
< Box key = { n . id } sx = { { display : 'flex' , justifyContent : 'space-between' , alignItems : 'center' , py : 0.75 , borderBottom : '1px solid #f1f5f9' } } >
< Box >
< Typography variant = "caption" sx = { { fontWeight : 500 } } > { n . companyName } < / Typography >
< Typography variant = "caption" color = "text.secondary" sx = { { display : 'block' } } >
{ n . requiredArea ? . min ? ? 0 } – { n . requiredArea ? . max ? ? 0 } m ²
{ n . budgetRange ? . maxPerSqm ? ` · max. CHF ${ n . budgetRange . maxPerSqm } /m² ` : '' }
< / Typography >
< / Box >
< Chip
label = { n . status === 'ACTIVE' ? 'Aktiv' : n . status === 'DRAFT' ? 'Entwurf' : n . status }
size = "small"
sx = { {
bgcolor : n.status === 'ACTIVE' ? '#f0fdf4' : '#f1f5f9' ,
color : n.status === 'ACTIVE' ? '#1a7a4a' : '#64748b' ,
fontSize : 10 , height : 18 ,
} }
/ >
< / Box >
) ) }
{ matchingNeeds . length > 4 && (
< Typography variant = "caption" color = "text.secondary" sx = { { mt : 0.75 , display : 'block' } } >
+ { matchingNeeds . length - 4 } weitere Suchprofile
< / Typography >
) }
< / >
) : (
< Typography variant = "body2" color = "text.secondary" >
Keine aktiven Suchprofile für diesen Typ und Standort .
< / Typography >
) }
{ intel && (
< Box sx = { { mt : 1.5 , p : 1.25 , bgcolor : '#f8fafc' , borderRadius : 1.5 } } >
< Typography variant = "caption" color = "text.secondary" >
Ø Vermietungsdauer vergleichbarer Objekte in { property . location . city } : { ' ' }
< strong style = { { color : '#1e3a5f' } } > { intel . avgDaysOnMarket } Tage < / strong >
< / Typography >
< / Box >
) }
< / Paper >
{ /* ── Selling arguments ── */ }
{ sellingArgs . length > 0 && (
< Paper sx = { { p : 2.5 } } >
< Typography variant = "subtitle2" sx = { { fontWeight : 700 , mb : 0.5 } } > Verkaufsargumente für das Gespräch < / Typography >
< Typography variant = "body2" color = "text.secondary" sx = { { mb : 1.5 } } >
Stärken dieses Objekts — maßgeschneidert auf typische Mieterwünsche
< / Typography >
{ strongArgs . length > 0 && (
< Box sx = { { mb : 1.5 } } >
< Typography variant = "caption" sx = { { fontWeight : 600 , color : '#1a7a4a' , display : 'block' , mb : 0.75 } } >
Starke Argumente
< / Typography >
{ strongArgs . map ( ( arg , i ) = > (
< Box key = { i } sx = { { display : 'flex' , gap : 1 , mb : 1 } } >
< CheckCircle size = { 15 } color = "#1a7a4a" style = { { marginTop : 2 , flexShrink : 0 } } / >
< Box >
< Typography variant = "body2" sx = { { fontWeight : 600 } } > { arg . title } < / Typography >
< Typography variant = "caption" color = "text.secondary" > { arg . detail } < / Typography >
< / Box >
< / Box >
) ) }
< / Box >
) }
{ mediumArgs . length > 0 && (
< Box >
< Typography variant = "caption" sx = { { fontWeight : 600 , color : '#d97706' , display : 'block' , mb : 0.75 } } >
Weitere Vorteile
< / Typography >
{ mediumArgs . map ( ( arg , i ) = > (
< Box key = { i } sx = { { display : 'flex' , gap : 1 , mb : 0.75 } } >
< CheckCircle size = { 14 } color = "#d97706" style = { { marginTop : 2 , flexShrink : 0 } } / >
< Box >
< Typography variant = "body2" sx = { { fontWeight : 500 } } > { arg . title } < / Typography >
< Typography variant = "caption" color = "text.secondary" > { arg . detail } < / Typography >
< / Box >
< / Box >
) ) }
< / Box >
) }
< / Paper >
) }
{ /* ── Proactive weakness handling ── */ }
{ weaknesses . length > 0 && (
< Paper sx = { { p : 2.5 } } >
< Typography variant = "subtitle2" sx = { { fontWeight : 700 , mb : 0.5 } } > Schwächen proaktiv adressieren < / Typography >
< Typography variant = "body2" color = "text.secondary" sx = { { mb : 1.5 } } >
Potenzielle Einwände kennen und entkräften — bevor der Interessent fragt
< / Typography >
{ weaknesses . map ( ( w , i ) = > (
< Box key = { i } sx = { { mb : 1.25 } } >
< Typography variant = "body2" sx = { { fontWeight : 600 , color : '#c0392b' , mb : 0.25 } } >
⚠ { w . issue }
< / Typography >
< Box sx = { { pl : 1.5 , borderLeft : '2px solid #e2e8f0' } } >
< Typography variant = "caption" color = "text.secondary" > → { w . mitigation } < / Typography >
< / Box >
{ i < weaknesses . length - 1 && < Divider sx = { { mt : 1.25 } } / > }
< / Box >
) ) }
< / Paper >
) }
{ /* ── Tenant fit ── */ }
{ intel && intel . dominantIndustryClusters . length > 0 && (
< Paper sx = { { p : 2.5 } } >
< Typography variant = "subtitle2" sx = { { fontWeight : 700 , mb : 0.5 } } > Welche Mieter passen ? < / Typography >
< Typography variant = "body2" color = "text.secondary" sx = { { mb : 1 } } >
Dominant präsente Branchen in { property . location . city } — hohes Match - Potenzial
< / Typography >
< Box sx = { { display : 'flex' , flexWrap : 'wrap' , gap : 0.75 } } >
{ intel . dominantIndustryClusters . map ( c = > (
< Chip key = { c } label = { c } size = "small"
sx = { { bgcolor : '#eff6ff' , color : '#1e3a5f' , fontSize : 11 , height : 24 , fontWeight : 500 } }
/ >
) ) }
< / Box >
< LinearProgress
variant = "determinate"
value = { intel . demandStrength === 'VERY_HIGH' ? 95 : intel.demandStrength === 'HIGH' ? 75 : intel.demandStrength === 'MEDIUM' ? 50 : 25 }
sx = { { mt : 1.5 , height : 6 , borderRadius : 3 , bgcolor : '#f1f5f9' , '& .MuiLinearProgress-bar' : { bgcolor : '#1e3a5f' } } }
/ >
< Typography variant = "caption" color = "text.secondary" sx = { { mt : 0.5 , display : 'block' } } >
Nachfragestärke : { ' ' }
< strong > { { LOW : 'Gering' , MEDIUM : 'Mittel' , HIGH : 'Hoch' , VERY_HIGH : 'Sehr hoch' } [ intel . demandStrength ] } < / strong >
< / Typography >
< / Paper >
) }
< / Box >
)
}