From c9324e9cc8a7274bc7d6d1984f0a03b0b7286a6f Mon Sep 17 00:00:00 2001 From: Benjamin Sutter Date: Thu, 21 May 2026 21:19:45 +0200 Subject: [PATCH] feat: expand NewListing with AI, soft factors, hard facts, images + MyListings manager MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - KI-Hilfe: text description → auto-fills all fields (parseListingText mock parser) - Lage & Ausstrahlung: 9 soft factor selects (Tief/Mittel/Hoch) mapped to scoring engine - Technische Details: floor, fit-out, parking, ceiling height - Bilder: URL list with add/remove - New /supply/my-listings page: list, status toggle, delete for DIRECT listings - Added sourceType filter to PropertyFilters + MockupPropertyProvider - Nav entry "Meine Inserate" added to supply sidebar Co-Authored-By: Claude Sonnet 4.6 --- src/App.tsx | 2 + src/components/layout/AppShell.tsx | 1 + src/pages/supply/MyListings.tsx | 247 +++++++++++++++ src/pages/supply/NewListing.tsx | 403 +++++++++++++++++++------ src/provider/IPropertyProvider.ts | 1 + src/provider/MockupPropertyProvider.ts | 1 + src/services/aiService.ts | 75 +++++ 7 files changed, 635 insertions(+), 95 deletions(-) create mode 100644 src/pages/supply/MyListings.tsx diff --git a/src/App.tsx b/src/App.tsx index 2f94c41..85db5b5 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -28,6 +28,7 @@ const DataQuality = lazy(() => import('./pages/supply/DataQuality')) const ReminderManager = lazy(() => import('./pages/supply/ReminderManager')) const MarketLeads = lazy(() => import('./pages/supply/MarketLeads')) const NewListing = lazy(() => import('./pages/supply/NewListing')) +const MyListings = lazy(() => import('./pages/supply/MyListings')) const AISearch = lazy(() => import('./pages/demand/AISearch')) const Results = lazy(() => import('./pages/demand/Results')) @@ -64,6 +65,7 @@ function App() { } /> } /> } /> + } /> {/* Demand Workspace */} diff --git a/src/components/layout/AppShell.tsx b/src/components/layout/AppShell.tsx index 8307067..e6aa327 100644 --- a/src/components/layout/AppShell.tsx +++ b/src/components/layout/AppShell.tsx @@ -89,6 +89,7 @@ const WORKSPACE_CONFIG: Record = { { path: '/supply/anfragen', label: 'Anfragencenter', icon: MessageSquare }, { path: '/supply/data-quality', label: 'Datenpflege', icon: CheckSquare }, { path: '/supply/market-intelligence', label: 'Markt Intelligence', icon: Radar }, + { path: '/supply/my-listings', label: 'Meine Inserate', icon: ClipboardList }, { path: '/supply/new-listing', label: 'Neues Inserat', icon: Plus }, ], }, diff --git a/src/pages/supply/MyListings.tsx b/src/pages/supply/MyListings.tsx new file mode 100644 index 0000000..8746b6f --- /dev/null +++ b/src/pages/supply/MyListings.tsx @@ -0,0 +1,247 @@ +import { useState } from 'react' +import { useNavigate } from 'react-router' +import { useQueryClient } from '@tanstack/react-query' +import { + Alert, + Box, + Button, + Chip, + CircularProgress, + Dialog, + DialogActions, + DialogContent, + DialogTitle, + IconButton, + Switch, + Tooltip, + Typography, +} from '@mui/material' +import { Plus, Trash2 } from 'lucide-react' +import { useProperties } from '../../hooks/useProperties' +import { propertyService } from '../../services/propertyService' +import type { Property } from '../../domain/property' + +const ASSET_LABELS: Record = { + OFFICE: 'Büro', + RETAIL: 'Einzelhandel', + LIGHT_INDUSTRIAL: 'Leichtindustrie', + LOGISTICS: 'Logistik', + PRODUCTION: 'Produktion', + MIXED: 'Gemischt', +} + +function formatDate(iso?: string) { + if (!iso) return '–' + return new Date(iso).toLocaleDateString('de-CH', { day: '2-digit', month: '2-digit', year: 'numeric' }) +} + +export default function MyListings() { + const navigate = useNavigate() + const qc = useQueryClient() + + const { data: listings = [], isLoading } = useProperties({ sourceType: 'DIRECT' }) + + const [togglingId, setTogglingId] = useState(null) + const [deletingId, setDeletingId] = useState(null) + const [confirmDelete, setConfirmDelete] = useState(null) + const [actionError, setActionError] = useState(null) + + async function handleToggleStatus(p: Property) { + setTogglingId(p.id) + setActionError(null) + try { + const next = p.status === 'ACTIVE' ? 'INACTIVE' : 'ACTIVE' + await propertyService.update(p.id, { status: next }) + qc.invalidateQueries({ queryKey: ['properties'] }) + } catch { + setActionError('Status konnte nicht geändert werden.') + } finally { + setTogglingId(null) + } + } + + async function handleDelete(p: Property) { + setDeletingId(p.id) + setActionError(null) + try { + await propertyService.remove(p.id) + qc.invalidateQueries({ queryKey: ['properties'] }) + } catch { + setActionError('Inserat konnte nicht gelöscht werden.') + } finally { + setDeletingId(null) + setConfirmDelete(null) + } + } + + return ( + + + + Meine Inserate + + Direkt erstellte Inserate — unabhängig vom Portfolio + + + + + + {actionError && ( + setActionError(null)}> + {actionError} + + )} + + {isLoading ? ( + + + + ) : listings.length === 0 ? ( + + Noch keine Inserate + + Erstellen Sie Ihr erstes direktes Inserat — ohne vollständiges Objekt im Portfolio. + + + + ) : ( + + {/* Header */} + + {['Inserat', 'Ort', 'Fläche', 'Preis/m²/J', 'Erstellt', 'Aktiv', ''].map(h => ( + + {h} + + ))} + + + {listings.map((p, i) => { + const isLast = i === listings.length - 1 + return ( + + {/* Title + type */} + + + {p.title} + + + + + {/* City */} + + {p.location.city} + + + {/* Area */} + + {p.areaSqm.toLocaleString('de-CH')} m² + + + {/* Rent */} + + CHF {p.rentPricePerSqm.toLocaleString('de-CH')} + + + {/* Created */} + + {formatDate(p.createdAt)} + + + {/* Status toggle */} + + {togglingId === p.id ? ( + + ) : ( + + handleToggleStatus(p)} + sx={{ '& .MuiSwitch-thumb': { width: 14, height: 14 } }} + /> + + )} + + + {/* Delete */} + + {deletingId === p.id ? ( + + ) : ( + + setConfirmDelete(p)} + sx={{ color: '#94a3b8', '&:hover': { color: '#ef4444' } }} + > + + + + )} + + + ) + })} + + )} + + {/* Delete confirmation dialog */} + setConfirmDelete(null)} maxWidth="xs" fullWidth> + Inserat löschen? + + + {confirmDelete?.title} wird unwiderruflich gelöscht. + + + + + + + + + ) +} diff --git a/src/pages/supply/NewListing.tsx b/src/pages/supply/NewListing.tsx index def9aa6..8508a92 100644 --- a/src/pages/supply/NewListing.tsx +++ b/src/pages/supply/NewListing.tsx @@ -5,15 +5,19 @@ import { Box, Button, Card, + Chip, CircularProgress, Divider, + IconButton, MenuItem, TextField, + Tooltip, Typography, } from '@mui/material' -import { ArrowLeft, CheckCircle } from 'lucide-react' -import { AssetType, ResultType, AvailabilityStatus } from '../../domain/enums' +import { ArrowLeft, CheckCircle, ImagePlus, Sparkles, X } from 'lucide-react' +import { AssetType, AvailabilityStatus, ResultType } from '../../domain/enums' import { propertyService } from '../../services/propertyService' +import { parseListingText } from '../../services/aiService' import type { CreatePropertyInput } from '../../domain/property' const ASSET_TYPE_LABELS: Record = { @@ -25,6 +29,37 @@ const ASSET_TYPE_LABELS: Record = { MIXED: 'Gemischt', } +const SOFT_FACTORS = [ + { key: 'prestige', label: 'Prestige / Adressqualität' }, + { key: 'accessibility', label: 'ÖV-Anbindung' }, + { key: 'visibility', label: 'Sichtbarkeit' }, + { key: 'footfall', label: 'Passantenfrequenz' }, + { key: 'talentAccess', label: 'Fachkräfte / Talent' }, + { key: 'esg', label: 'ESG / Nachhaltigkeit' }, + { key: 'flexibility', label: 'Flexibilität Grundriss' }, + { key: 'expansionPotential', label: 'Expansionspotenzial' }, + { key: 'taxEnvironment', label: 'Steuerumgebung' }, +] + +const LEVEL_OPTIONS = [ + { value: '', label: 'Keine Angabe' }, + { value: 'LOW', label: 'Tief' }, + { value: 'MEDIUM', label: 'Mittel' }, + { value: 'HIGH', label: 'Hoch' }, +] + +const LEVEL_TO_SCORE: Record = { + LOW: 3, MEDIUM: 5, HIGH: 8, '': undefined, +} + +const FIT_OUT_OPTIONS = [ + { value: '', label: 'Keine Angabe' }, + { value: 'SHELL', label: 'Rohbau' }, + { value: 'BASIC', label: 'Basis-Ausbau' }, + { value: 'FULL', label: 'Vollausbau' }, + { value: 'PREMIUM', label: 'Premiumausbau' }, +] + interface LocationState { prefill?: { assetType?: string @@ -44,25 +79,75 @@ export default function NewListing() { const { state } = useLocation() as { state: LocationState | null } const pre = state?.prefill ?? {} - const [assetType, setAssetType] = useState(pre.assetType ?? 'OFFICE') - const [street, setStreet] = useState(pre.street ?? '') + // Core fields + const [assetType, setAssetType] = useState(pre.assetType ?? 'OFFICE') + const [street, setStreet] = useState(pre.street ?? '') const [houseNumber, setHouseNumber] = useState(pre.houseNumber ?? '') - const [postalCode, setPostalCode] = useState(pre.postalCode ?? '') - const [city, setCity] = useState(pre.city ?? '') - const [areaSqm, setAreaSqm] = useState(pre.areaSqm ? String(pre.areaSqm) : '') - const [rentPerSqm, setRentPerSqm] = useState(pre.rentPricePerSqm ? String(pre.rentPricePerSqm) : '') + const [postalCode, setPostalCode] = useState(pre.postalCode ?? '') + const [city, setCity] = useState(pre.city ?? '') + const [areaSqm, setAreaSqm] = useState(pre.areaSqm ? String(pre.areaSqm) : '') + const [rentPerSqm, setRentPerSqm] = useState(pre.rentPricePerSqm ? String(pre.rentPricePerSqm) : '') const [availableFrom, setAvailableFrom] = useState('') const [description, setDescription] = useState('') - const [contactName, setContactName] = useState('') + const [contactName, setContactName] = useState('') const [contactEmail, setContactEmail] = useState('') const [contactPhone, setContactPhone] = useState('') + // Soft factors + const [softLevels, setSoftLevels] = useState>( + Object.fromEntries(SOFT_FACTORS.map(f => [f.key, ''])) + ) + + // Hard facts + const [floor, setFloor] = useState('') + const [fitOut, setFitOut] = useState('') + const [parking, setParking] = useState('') + const [ceilingHeight, setCeilingHeight] = useState('') + + // Images + const [images, setImages] = useState([]) + const [imageInput, setImageInput] = useState('') + + // AI + const [aiText, setAiText] = useState('') + const [aiParsing, setAiParsing] = useState(false) + const [aiApplied, setAiApplied] = useState(false) + + // Submit const [submitting, setSubmitting] = useState(false) - const [error, setError] = useState(null) - const [created, setCreated] = useState(false) + const [error, setError] = useState(null) + const [created, setCreated] = useState(false) const isPrefilled = !!pre.propertyId + async function handleAiParse() { + if (!aiText.trim()) return + setAiParsing(true) + try { + const parsed = await parseListingText(aiText) + if (parsed.assetType) setAssetType(parsed.assetType) + if (parsed.areaSqm) setAreaSqm(String(parsed.areaSqm)) + if (parsed.rentPerSqm) setRentPerSqm(String(parsed.rentPerSqm)) + if (parsed.city && !city) setCity(parsed.city) + if (parsed.fitOut) setFitOut(parsed.fitOut) + if (parsed.parking) setParking(String(parsed.parking)) + if (parsed.softLevels) { + setSoftLevels(prev => ({ ...prev, ...parsed.softLevels })) + } + setAiApplied(true) + } finally { + setAiParsing(false) + } + } + + function addImage() { + const url = imageInput.trim() + if (url && !images.includes(url)) { + setImages(prev => [...prev, url]) + } + setImageInput('') + } + function validate(): string | null { if (!street.trim()) return 'Strasse erforderlich' if (!postalCode.trim()) return 'PLZ erforderlich' @@ -81,19 +166,47 @@ export default function NewListing() { const area = Number(areaSqm) const rent = Number(rentPerSqm) + const sf = { + prestigeScore: LEVEL_TO_SCORE[softLevels.prestige ?? ''], + commuterAccessScore: LEVEL_TO_SCORE[softLevels.accessibility ?? ''], + visibilityScore: LEVEL_TO_SCORE[softLevels.visibility ?? ''], + footfallScore: LEVEL_TO_SCORE[softLevels.footfall ?? ''], + talentAccessScore: LEVEL_TO_SCORE[softLevels.talentAccess ?? ''], + esgScore: LEVEL_TO_SCORE[softLevels.esg ?? ''], + flexibilityScore: LEVEL_TO_SCORE[softLevels.flexibility ?? ''], + expansionPotentialScore: LEVEL_TO_SCORE[softLevels.expansionPotential ?? ''], + taxEnvironmentScore: LEVEL_TO_SCORE[softLevels.taxEnvironment ?? ''], + } + + const hf = { + floor: floor ? parseInt(floor) : undefined, + fitOut: (fitOut || undefined) as 'SHELL' | 'BASIC' | 'FULL' | 'PREMIUM' | undefined, + parking: parking ? parseInt(parking) : undefined, + ceilingHeightM: ceilingHeight ? parseFloat(ceilingHeight) : undefined, + } + const input: CreatePropertyInput = { title: `${ASSET_TYPE_LABELS[assetType] ?? assetType} · ${city}`, assetType: assetType as typeof AssetType[keyof typeof AssetType], resultType: ResultType.EXTERNAL_MARKET, sourceType: 'DIRECT', location: { city, country: 'CH' }, - address: { street: street.trim(), houseNumber: houseNumber.trim(), postalCode: postalCode.trim(), city: city.trim(), country: 'CH' }, + address: { + street: street.trim(), + houseNumber: houseNumber.trim(), + postalCode: postalCode.trim(), + city: city.trim(), + country: 'CH', + }, areaSqm: area, rentPricePerSqm: rent, availabilityDate: availableFrom || new Date().toISOString().slice(0, 10), availabilityStatus: availableFrom ? AvailabilityStatus.AVAILABLE_SOON : AvailabilityStatus.AVAILABLE_NOW, confidenceScore: 1.0, description: description.trim() || undefined, + softFactors: sf, + hardFacts: hf, + images: images.length > 0 ? images : undefined, dataQuality: { score: 1.0, missingCriticalFields: [], @@ -114,24 +227,31 @@ export default function NewListing() { } } + function resetForm() { + setAssetType('OFFICE') + setStreet(''); setHouseNumber(''); setPostalCode(''); setCity('') + setAreaSqm(''); setRentPerSqm(''); setAvailableFrom(''); setDescription('') + setContactName(''); setContactEmail(''); setContactPhone('') + setSoftLevels(Object.fromEntries(SOFT_FACTORS.map(f => [f.key, '']))) + setFloor(''); setFitOut(''); setParking(''); setCeilingHeight('') + setImages([]); setImageInput('') + setAiText(''); setAiApplied(false) + setCreated(false) + } + if (created) { return ( - + Inserat erstellt - Das Inserat wurde erfolgreich veröffentlicht und ist jetzt für passende Suchanfragen sichtbar. + Das Inserat wurde veröffentlicht und ist für passende Suchanfragen sichtbar. - - @@ -140,7 +260,8 @@ export default function NewListing() { } return ( - + + {/* Header */} + + + {/* Flächendetails */} Flächendetails - setAssetType(e.target.value)} - size="small" - fullWidth + select label="Flächentyp" value={assetType} + onChange={e => setAssetType(e.target.value)} size="small" fullWidth > {Object.entries(ASSET_TYPE_LABELS).map(([v, l]) => ( {l} @@ -178,116 +333,174 @@ export default function NewListing() { setAreaSqm(e.target.value)} - size="small" - type="number" - inputProps={{ min: 1 }} - fullWidth + size="small" type="number" inputProps={{ min: 1 }} fullWidth /> setRentPerSqm(e.target.value)} - size="small" - type="number" - inputProps={{ min: 1 }} - fullWidth + size="small" type="number" inputProps={{ min: 1 }} fullWidth /> setAvailableFrom(e.target.value)} - size="small" - type="date" - slotProps={{ inputLabel: { shrink: true } }} - fullWidth + size="small" type="date" + slotProps={{ inputLabel: { shrink: true } }} fullWidth /> - setDescription(e.target.value)} - size="small" - multiline - rows={3} - fullWidth - sx={{ mt: 2 }} + size="small" multiline rows={3} fullWidth sx={{ mt: 2 }} /> + {/* Adresse */} Adresse setStreet(e.target.value)} - size="small" - fullWidth + label="Strasse" value={street} + onChange={e => setStreet(e.target.value)} size="small" fullWidth /> setHouseNumber(e.target.value)} - size="small" - fullWidth + label="Nr." value={houseNumber} + onChange={e => setHouseNumber(e.target.value)} size="small" fullWidth /> setPostalCode(e.target.value)} - size="small" - fullWidth + label="PLZ" value={postalCode} + onChange={e => setPostalCode(e.target.value)} size="small" fullWidth /> setCity(e.target.value)} - size="small" - fullWidth + label="Ort" value={city} + onChange={e => setCity(e.target.value)} size="small" fullWidth /> + {/* Lage & Ausstrahlung */} + + + Lage & Ausstrahlung + + Diese Angaben verbessern die Match-Qualität erheblich. + + + + {SOFT_FACTORS.map(({ key, label }) => ( + setSoftLevels(prev => ({ ...prev, [key]: e.target.value }))} + size="small" + fullWidth + > + {LEVEL_OPTIONS.map(o => ( + {o.label} + ))} + + ))} + + + + {/* Technische Details */} + + Technische Details (optional) + + setFloor(e.target.value)} + size="small" type="number" fullWidth + placeholder="0 = EG" + /> + setFitOut(e.target.value)} size="small" fullWidth + > + {FIT_OUT_OPTIONS.map(o => ( + {o.label} + ))} + + setParking(e.target.value)} + size="small" type="number" inputProps={{ min: 0 }} fullWidth + /> + setCeilingHeight(e.target.value)} + size="small" type="number" inputProps={{ step: 0.1, min: 2 }} fullWidth + /> + + + + {/* Bilder */} + + Bilder (optional) + + setImageInput(e.target.value)} + onKeyDown={e => { if (e.key === 'Enter') { e.preventDefault(); addImage() } }} + size="small" + fullWidth + placeholder="https://…" + /> + + + {images.length > 0 && ( + + {images.map((url, i) => ( + 40 ? url.slice(0, 40) + '…' : url} + size="small" + onDelete={() => setImages(prev => prev.filter((_, j) => j !== i))} + sx={{ maxWidth: 300 }} + /> + ))} + + )} + + + {/* Kontakt */} Kontakt (optional) setContactName(e.target.value)} - size="small" - fullWidth + label="Name" value={contactName} + onChange={e => setContactName(e.target.value)} size="small" fullWidth /> setContactPhone(e.target.value)} - size="small" - fullWidth + label="Telefon" value={contactPhone} + onChange={e => setContactPhone(e.target.value)} size="small" fullWidth /> setContactEmail(e.target.value)} - size="small" - type="email" - fullWidth - sx={{ gridColumn: '1 / -1' }} + size="small" type="email" fullWidth sx={{ gridColumn: '1 / -1' }} /> - {error && ( - {error} - )} + {error && {error}} diff --git a/src/provider/IPropertyProvider.ts b/src/provider/IPropertyProvider.ts index 4401a36..cbccd87 100644 --- a/src/provider/IPropertyProvider.ts +++ b/src/provider/IPropertyProvider.ts @@ -4,6 +4,7 @@ import type { AssetType, ResultType } from '../domain/enums' export interface PropertyFilters { assetType?: AssetType resultType?: ResultType + sourceType?: string city?: string minAreaSqm?: number maxRentPerSqm?: number diff --git a/src/provider/MockupPropertyProvider.ts b/src/provider/MockupPropertyProvider.ts index b3af131..a613acc 100644 --- a/src/provider/MockupPropertyProvider.ts +++ b/src/provider/MockupPropertyProvider.ts @@ -13,6 +13,7 @@ export const MockupPropertyProvider: IPropertyProvider = { if (filters?.city) results = results.filter(p => p.location.city.toLowerCase().includes(filters.city!.toLowerCase())) if (filters?.minAreaSqm) results = results.filter(p => p.areaSqm >= filters.minAreaSqm!) if (filters?.maxRentPerSqm) results = results.filter(p => p.rentPricePerSqm <= filters.maxRentPerSqm!) + if (filters?.sourceType) results = results.filter(p => p.sourceType === filters.sourceType) if (filters?.organizationId) results = results.filter(p => p.organizationId === filters.organizationId) return results }, diff --git a/src/services/aiService.ts b/src/services/aiService.ts index 318a416..a5f8f6d 100644 --- a/src/services/aiService.ts +++ b/src/services/aiService.ts @@ -445,3 +445,78 @@ export const aiService = { } }, } + +// ── Listing Parser (Supply Side) ────────────────────────────────────────────── + +export interface ParsedListingData { + assetType?: string + areaSqm?: number + rentPerSqm?: number + city?: string + softLevels?: Record + parking?: number + fitOut?: string +} + +export async function parseListingText(text: string): Promise { + await new Promise(r => setTimeout(r, 900)) + const t = text.toLowerCase() + const result: ParsedListingData = {} + + // Asset type + if (t.includes('laden') || t.includes('retail') || t.includes('shop') || t.includes('geschäft')) { + result.assetType = 'RETAIL' + } else if (t.includes('lager') || t.includes('logistik')) { + result.assetType = 'LOGISTICS' + } else if (t.includes('produktion') || t.includes('industrie') || t.includes('werkstatt')) { + result.assetType = 'PRODUCTION' + } else { + result.assetType = 'OFFICE' + } + + // Area + const areaMatch = text.match(/(\d{2,5})\s*m²/) ?? text.match(/(\d{2,5})\s*Quadratmeter/) + if (areaMatch) result.areaSqm = parseInt(areaMatch[1]) + + // Price — if raw < 500 assume monthly → convert to annual + const priceMatch = text.match(/(\d{2,4})\s*(?:CHF|Fr\.?)\s*\/\s*m²/) ?? text.match(/CHF\s*(\d{2,4})/) + if (priceMatch) { + const raw = parseInt(priceMatch[1]) + result.rentPerSqm = raw < 500 ? raw * 12 : raw + } + + // City + const CITIES = ['Zürich', 'Bern', 'Basel', 'Genf', 'Lausanne', 'Zug', 'Winterthur', 'St. Gallen', 'Lugano', 'Luzern', 'Biel', 'Thun', 'Kloten', 'Opfikon', 'Uster'] + for (const city of CITIES) { + if (t.includes(city.toLowerCase())) { result.city = city; break } + } + + // Soft factors + const soft: Record = {} + soft.prestige = (t.includes('zentrum') || t.includes('innenstadt') || t.includes('hauptbahnhof') || t.includes('repräsentativ') || t.includes('prestige')) + ? 'HIGH' : (t.includes('gewerbegebiet') || t.includes('peripherie') || t.includes('industriezone')) + ? 'LOW' : 'MEDIUM' + soft.accessibility = (t.includes('bahnhof') || t.includes('s-bahn') || t.includes('tram') || t.includes('öv') || t.includes('zentrum')) + ? 'HIGH' : (t.includes('autobahn') || t.includes('gewerbegebiet')) + ? 'LOW' : 'MEDIUM' + if (result.assetType === 'RETAIL') { + soft.visibility = t.includes('fussgänger') || t.includes('passanten') || t.includes('frequenz') ? 'HIGH' : 'MEDIUM' + soft.footfall = soft.visibility + } + if (t.includes('nachhaltig') || t.includes('minergie') || t.includes('esg') || t.includes('zertifiziert')) soft.esg = 'HIGH' + if (t.includes('expansion') || t.includes('erweiter') || t.includes('wachstum')) soft.expansionPotential = 'HIGH' + if (result.city === 'Zug') soft.taxEnvironment = 'HIGH' + if (t.includes('hochwertig') || t.includes('premium') || t.includes('erstklassig')) { + soft.prestige = 'HIGH' + result.fitOut = 'PREMIUM' + } else if (t.includes('einfach') || t.includes('standard-ausbau')) { + result.fitOut = 'BASIC' + } + result.softLevels = soft + + // Parking + const parkingMatch = text.match(/(\d+)\s*Parkplätze?/) ?? text.match(/(\d+)\s*PP/) + if (parkingMatch) result.parking = parseInt(parkingMatch[1]) + + return result +}