feat: expand NewListing with AI, soft factors, hard facts, images + MyListings manager
- 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 <noreply@anthropic.com>
This commit is contained in:
@@ -445,3 +445,78 @@ export const aiService = {
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
// ── Listing Parser (Supply Side) ──────────────────────────────────────────────
|
||||
|
||||
export interface ParsedListingData {
|
||||
assetType?: string
|
||||
areaSqm?: number
|
||||
rentPerSqm?: number
|
||||
city?: string
|
||||
softLevels?: Record<string, string>
|
||||
parking?: number
|
||||
fitOut?: string
|
||||
}
|
||||
|
||||
export async function parseListingText(text: string): Promise<ParsedListingData> {
|
||||
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<string, string> = {}
|
||||
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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user