feat: Reale Jahresbelastung, must-have fixes, fitOut data coverage
Reale Jahresbelastung (Change 6): - FitOutCostPanel zeigt Jahresmiete + amortisierte Ausbaukosten für alle fitOut-Werte - FULL/PREMIUM: Ausbau CHF 0 (bezugsfertig), SHELL/BASIC: CRB/BKP-Richtwerte amortisiert über 5 J. - Match-Card-Chip zeigt geschätzte Investition (orange wenn >100k) - FitOutInvestment-Typ + calcFitOutInvestment() in fitOutUtils.ts - BackendAIService + MockAIService mit generateFitOutAdvice (IAIService-Interface) Must-have Kriterien (Nicht prüfbar Fix): - ÖV-Anbindung: Minutengrenze aus Freitext extrahiert, gegen publicTransportMinutes geprüft - Mindestfläche: m²-Wert aus Freitext extrahiert, gegen areaSqm geprüft - Ausbaugrad: neues Keyword-Rule für FULL/PREMIUM fitOut-Datenpflege: - fitOut-Werte zu 32 fehlenden Properties ergänzt (Logistik=SHELL, Standard=BASIC, Modern=FULL) - MAB-Werte (200/150/250 CHF/m²) zu 3 BASIC-Objekten hinzugefügt Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { useEffect } from 'react'
|
||||
import { Box } from '@mui/material'
|
||||
import { MapContainer, TileLayer, Marker, Popup, useMap } from 'react-leaflet'
|
||||
import { Box, Typography } from '@mui/material'
|
||||
import { MapContainer, TileLayer, Marker, Popup, Circle, useMap } from 'react-leaflet'
|
||||
import L from 'leaflet'
|
||||
import 'leaflet/dist/leaflet.css'
|
||||
|
||||
@@ -12,11 +12,21 @@ L.Icon.Default.mergeOptions({
|
||||
shadowUrl: new URL('leaflet/dist/images/marker-shadow.png', import.meta.url).href,
|
||||
})
|
||||
|
||||
const searchIcon = L.divIcon({
|
||||
className: '',
|
||||
html: `<div style="width:14px;height:14px;border-radius:50%;background:#7c3aed;border:3px solid white;box-shadow:0 2px 6px rgba(124,58,237,0.5)"></div>`,
|
||||
iconAnchor: [7, 7],
|
||||
})
|
||||
|
||||
interface Props {
|
||||
lat: number
|
||||
lng: number
|
||||
label?: string
|
||||
height?: number
|
||||
searchCenterLat?: number
|
||||
searchCenterLng?: number
|
||||
searchRadiusKm?: number
|
||||
searchLabel?: string
|
||||
}
|
||||
|
||||
function RecenterOnChange({ lat, lng }: { lat: number; lng: number }) {
|
||||
@@ -27,34 +37,85 @@ function RecenterOnChange({ lat, lng }: { lat: number; lng: number }) {
|
||||
return null
|
||||
}
|
||||
|
||||
export function PropertyMap({ lat, lng, label, height = 260 }: Props) {
|
||||
function FitToRadius({ propLat, propLng, searchLat, searchLng, radiusKm }: {
|
||||
propLat: number; propLng: number
|
||||
searchLat: number; searchLng: number; radiusKm: number
|
||||
}) {
|
||||
const map = useMap()
|
||||
useEffect(() => {
|
||||
const circleBounds = L.latLng(searchLat, searchLng).toBounds(radiusKm * 1000 * 2)
|
||||
const bounds = circleBounds.extend([propLat, propLng])
|
||||
map.fitBounds(bounds, { padding: [30, 30] })
|
||||
}, [map, propLat, propLng, searchLat, searchLng, radiusKm])
|
||||
return null
|
||||
}
|
||||
|
||||
export function PropertyMap({ lat, lng, label, height = 260, searchCenterLat, searchCenterLng, searchRadiusKm, searchLabel }: Props) {
|
||||
const hasSearch = searchCenterLat !== undefined && searchCenterLng !== undefined && searchRadiusKm !== undefined
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
height,
|
||||
width: '100%',
|
||||
overflow: 'hidden',
|
||||
flexShrink: 0,
|
||||
// Ensure leaflet controls appear above MUI elements
|
||||
'& .leaflet-container': { height: '100%', width: '100%', zIndex: 0 },
|
||||
'& .leaflet-control': { zIndex: 1 },
|
||||
}}
|
||||
>
|
||||
<Box sx={{ position: 'relative', height, width: '100%', overflow: 'hidden', flexShrink: 0,
|
||||
'& .leaflet-container': { height: '100%', width: '100%', zIndex: 0 },
|
||||
'& .leaflet-control': { zIndex: 1 },
|
||||
}}>
|
||||
<MapContainer
|
||||
center={[lat, lng]}
|
||||
zoom={14}
|
||||
zoom={hasSearch ? 10 : 14}
|
||||
scrollWheelZoom
|
||||
style={{ height: '100%', width: '100%' }}
|
||||
>
|
||||
<RecenterOnChange lat={lat} lng={lng} />
|
||||
{hasSearch
|
||||
? <FitToRadius propLat={lat} propLng={lng} searchLat={searchCenterLat!} searchLng={searchCenterLng!} radiusKm={searchRadiusKm!} />
|
||||
: <RecenterOnChange lat={lat} lng={lng} />
|
||||
}
|
||||
<TileLayer
|
||||
attribution='© <a href="https://www.openstreetmap.org/copyright" target="_blank">OpenStreetMap</a>'
|
||||
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
|
||||
/>
|
||||
|
||||
{/* Search radius circle */}
|
||||
{hasSearch && (
|
||||
<Circle
|
||||
center={[searchCenterLat!, searchCenterLng!]}
|
||||
radius={searchRadiusKm! * 1000}
|
||||
pathOptions={{ color: '#7c3aed', fillColor: '#7c3aed', fillOpacity: 0.07, weight: 2, dashArray: '6 4' }}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Search center marker */}
|
||||
{hasSearch && (
|
||||
<Marker position={[searchCenterLat!, searchCenterLng!]} icon={searchIcon}>
|
||||
<Popup>{searchLabel ?? `Suchradius: ${searchRadiusKm} km`}</Popup>
|
||||
</Marker>
|
||||
)}
|
||||
|
||||
{/* Property marker */}
|
||||
<Marker position={[lat, lng]}>
|
||||
{label && <Popup>{label}</Popup>}
|
||||
</Marker>
|
||||
</MapContainer>
|
||||
|
||||
{/* Legend overlay */}
|
||||
{hasSearch && (
|
||||
<Box sx={{
|
||||
position: 'absolute', bottom: 8, left: 8, zIndex: 500,
|
||||
bgcolor: 'rgba(255,255,255,0.92)', borderRadius: 1.5,
|
||||
px: 1.25, py: 0.625, display: 'flex', flexDirection: 'column', gap: 0.4,
|
||||
boxShadow: '0 1px 6px rgba(0,0,0,0.12)',
|
||||
backdropFilter: 'blur(4px)',
|
||||
}}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.75 }}>
|
||||
<Box sx={{ width: 10, height: 10, borderRadius: '50%', bgcolor: '#7c3aed', flexShrink: 0 }} />
|
||||
<Typography sx={{ fontSize: '0.67rem', color: '#5b21b6', fontWeight: 700 }}>
|
||||
{searchLabel ?? 'Suchzentrum'} · {searchRadiusKm} km Radius
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.75 }}>
|
||||
<Box sx={{ width: 10, height: 10, borderRadius: '50%', bgcolor: '#1e3a5f', flexShrink: 0 }} />
|
||||
<Typography sx={{ fontSize: '0.67rem', color: '#334155' }}>Objekt</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user