feat: Suchabo inline contact flow — chip opens need detail + OfferWizard

- Add >80% STRONG matches for all 6 direct listings (match-072–075)
- Add 4 new Needs (need-012–015) tailored to each previously unmatched listing
- Add 8 new LatentNeeds (lneed-007–014) for all needs in direct listing matches
- Add need→latentNeed mapping (src/lib/needToLatentNeedMap.ts)
- MyListings: clicking Suchabo chip opens dialog with company name, score,
  PublicNeedDetail view and OfferWizard — no navigation to Anfragencenter needed

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Benjamin Sutter
2026-05-24 23:08:41 +02:00
parent a9ee53f185
commit cd220ed282
5 changed files with 672 additions and 4 deletions
+11
View File
@@ -0,0 +1,11 @@
/** Maps a Need id (need-XXX) to its corresponding LatentNeed id (lneed-XXX). */
export const NEED_TO_LATENT_NEED: Record<string, string> = {
'need-001': 'lneed-007',
'need-002': 'lneed-008',
'need-004': 'lneed-009',
'need-005': 'lneed-010',
'need-012': 'lneed-011',
'need-013': 'lneed-012',
'need-014': 'lneed-013',
'need-015': 'lneed-014',
}
+220
View File
@@ -146,6 +146,226 @@ export const mockLatentNeeds: LatentNeed[] = [
createdAt: '2026-05-09T08:15:00Z', createdAt: '2026-05-09T08:15:00Z',
}, },
// ─── Suchabos linked to direct listing matches ────────────────────────────
{
id: 'lneed-007',
title: 'Modernes Büro in Zürich für 6001000m²',
tenantCompany: 'Innovatech AG',
assetType: AssetType.OFFICE,
desiredLocation: 'Zürich-West / Kreis 5',
sizeRange: { min: 600, max: 1000 },
budgetRange: { min: 32, max: 45 },
timing: 'Bezug ab Herbst 2025, flexibel',
mustHaveCriteria: [
'ÖV-Anbindung unter 6 Min',
'Mindestfläche 600m²',
'Moderner Ausbau',
],
weightedPreferences: [
{ criterion: 'Lage Zürich-West', weight: 0.35 },
{ criterion: 'Ausbaustandard', weight: 0.25, description: 'Modern, repräsentativ' },
{ criterion: 'Erweiterungsoption', weight: 0.20 },
{ criterion: 'ESG-Standard', weight: 0.10 },
{ criterion: 'Parkplätze', weight: 0.10 },
],
aiSummary: 'Technologieunternehmen mit solidem Wachstum sucht Bürofläche in Zürich-West. Hohe Abschlussbereitschaft bei passendem Objekt.',
publicVisibility: true,
status: 'public',
createdAt: '2026-04-15T10:00:00Z',
},
{
id: 'lneed-008',
title: 'Logistikfläche Basel-Region 15004000m²',
tenantCompany: 'Schweizer Logistik GmbH',
assetType: AssetType.LOGISTICS,
desiredLocation: 'Basel / Muttenz / Pratteln / Reinach BL',
sizeRange: { min: 1500, max: 4000 },
budgetRange: { min: 12, max: 18 },
timing: 'Bezug ab September 2025',
mustHaveCriteria: [
'Autobahnanschluss unter 5 Min',
'Rampe / Andienung vorhanden',
'Hallenhöhe min 6m',
],
weightedPreferences: [
{ criterion: 'Rampenanzahl', weight: 0.30 },
{ criterion: 'Lkw-Wendekreis', weight: 0.25 },
{ criterion: 'Erweiterungspotenzial', weight: 0.20 },
{ criterion: '24/7-Betrieb', weight: 0.15 },
{ criterion: 'Sprinkleranlage', weight: 0.10 },
],
aiSummary: 'Etablierter Logistiker mit klaren Anforderungen. Region Basel priorisiert. Sehr gute Matchbasis mit verfügbaren Hallenflächen.',
publicVisibility: true,
status: 'public',
createdAt: '2026-03-20T14:00:00Z',
},
{
id: 'lneed-009',
title: 'Premium-Retailfläche Zürich Innenstadt',
tenantCompany: 'Retailer Zürich AG',
assetType: AssetType.RETAIL,
desiredLocation: 'Zürich Innenstadt / Bahnhofstrasse / Niederdorf',
sizeRange: { min: 200, max: 500 },
budgetRange: { min: 60, max: 100 },
timing: 'Bezug ab September 2025, hohe Dringlichkeit',
mustHaveCriteria: [
'Laufkundschaft und Passantenfrequenz',
'Schaufensterfront',
'Erdgeschoss',
],
weightedPreferences: [
{ criterion: 'Fussgängerfrequenz', weight: 0.35 },
{ criterion: 'Lagequalität', weight: 0.30, description: 'Zürich Innenstadt' },
{ criterion: 'Sichtbarkeit', weight: 0.20 },
{ criterion: 'Deckenhöhe', weight: 0.10 },
{ criterion: 'Lagerfläche', weight: 0.05 },
],
aiSummary: 'Retailer mit starkem Markenbewusstsein sucht Toplage in Zürich. Klare Präferenz Innenstadt, solider Abschluss bei Matching.',
publicVisibility: true,
status: 'public',
createdAt: '2026-03-10T11:00:00Z',
},
{
id: 'lneed-010',
title: 'Büro 300700m² für Tech-Startup Zug/Zürich',
tenantCompany: 'TechStart GmbH',
assetType: AssetType.OFFICE,
desiredLocation: 'Zug / Zürich / Baar',
sizeRange: { min: 300, max: 700 },
budgetRange: { min: 30, max: 48 },
timing: 'Bezug ab Oktober 2025, flexibel bis Q2 2026',
mustHaveCriteria: [
'Moderner Ausbau',
'Schnelles Internet / Glasfaser',
'Fahrradabstellplätze',
],
weightedPreferences: [
{ criterion: 'Tech-Umfeld / Startup-Nähe', weight: 0.30 },
{ criterion: 'ÖV-Anbindung', weight: 0.25 },
{ criterion: 'Flexibler Grundriss', weight: 0.20 },
{ criterion: 'Steuerumfeld', weight: 0.15, description: 'Zug bevorzugt' },
{ criterion: 'ESG-Qualität', weight: 0.10 },
],
aiSummary: 'Wachsendes Tech-Startup mit flexibler Lage zwischen Zug und Zürich. Hohe Bereitschaft bei modernem Objekt mit gutem Preis-Leistungs-Verhältnis.',
publicVisibility: true,
status: 'public',
createdAt: '2026-04-28T13:00:00Z',
},
{
id: 'lneed-011',
title: 'Gewerbe-/Lagerfläche Pratteln 400750m²',
tenantCompany: 'Gewerbe Solutions AG',
assetType: AssetType.LIGHT_INDUSTRIAL,
desiredLocation: 'Pratteln / Muttenz / Schweizerhalle',
sizeRange: { min: 400, max: 750 },
budgetRange: { min: 10, max: 14 },
timing: 'Bezug ab April 2026',
mustHaveCriteria: [
'Lkw-Zufahrt',
'Hallenhöhe min 5m',
'Drehstrom vorhanden',
],
weightedPreferences: [
{ criterion: 'Autobahnnähe', weight: 0.30 },
{ criterion: 'Aussenanlagefläche', weight: 0.25 },
{ criterion: 'Erweiterungspotenzial', weight: 0.20 },
{ criterion: 'Parkplätze', weight: 0.15 },
{ criterion: 'Büroanteil', weight: 0.10, description: 'Kleines Büro on-site gewünscht' },
],
aiSummary: 'Gewerbebetrieb mit klaren Anforderungen sucht Fläche im Raum Pratteln. Gute Ausgangslage für verfügbare Gewerbeliegenschaften.',
publicVisibility: true,
status: 'public',
createdAt: '2026-03-01T09:00:00Z',
},
{
id: 'lneed-012',
title: 'Büro 180320m² Bern Zentrum / Bahnhof',
tenantCompany: 'Bern Consulting AG',
assetType: AssetType.OFFICE,
desiredLocation: 'Bern Bollwerk / Bahnhof / Innenstadt',
sizeRange: { min: 180, max: 320 },
budgetRange: { min: 20, max: 30 },
timing: 'Bezug ab Mai 2026',
mustHaveCriteria: [
'ÖV-Anbindung unter 5 Min zum Bahnhof',
'Repräsentative Lage',
'Konferenzraum',
],
weightedPreferences: [
{ criterion: 'Bahnhofsnähe', weight: 0.35, description: 'Kundenmeetings erfordern zentrale Lage' },
{ criterion: 'Ausbaustandard', weight: 0.25 },
{ criterion: 'Flexibilität', weight: 0.20 },
{ criterion: 'Natürliches Licht', weight: 0.10 },
{ criterion: 'IT-Infrastruktur', weight: 0.10 },
],
aiSummary: 'Beratungsunternehmen sucht repräsentatives Büro nahe Bern HB. Zentrale Lage ist entscheidend für Kundenkontakte. Guter Match mit Berner Citylagen.',
publicVisibility: true,
status: 'public',
createdAt: '2026-04-05T10:00:00Z',
},
{
id: 'lneed-013',
title: 'Verkaufsfläche Winterthur Zentrum 150250m²',
tenantCompany: 'Winterthur Handel GmbH',
assetType: AssetType.RETAIL,
desiredLocation: 'Winterthur Zentrum / Stadthausstrasse / Altstadt',
sizeRange: { min: 150, max: 250 },
budgetRange: { min: 25, max: 40 },
timing: 'Bezug ab Juni 2026',
mustHaveCriteria: [
'Schaufensterfront',
'Erdgeschoss',
'Fussgängerfrequenz',
],
weightedPreferences: [
{ criterion: 'Lage Innenstadt Winterthur', weight: 0.40 },
{ criterion: 'Schaufenstergrösse', weight: 0.25 },
{ criterion: 'Deckenhöhe', weight: 0.15 },
{ criterion: 'Lagerraum', weight: 0.10 },
{ criterion: 'Barrierefrei', weight: 0.10 },
],
aiSummary: 'Lokaler Handelsbetrieb sucht Fläche in Winterthur Innenstadt. Klare Anforderungen, realistisches Budget. Guter Match mit Stadtzentrum-Angeboten.',
publicVisibility: true,
status: 'public',
createdAt: '2026-03-20T11:00:00Z',
},
{
id: 'lneed-014',
title: 'Bürofläche 350600m² Luzern',
tenantCompany: 'Luzern Advisory GmbH',
assetType: AssetType.OFFICE,
desiredLocation: 'Luzern / Luzern-West / Emmen',
sizeRange: { min: 350, max: 600 },
budgetRange: { min: 18, max: 26 },
timing: 'Bezug ab Juni 2026, flexibel bis Januar 2027',
mustHaveCriteria: [
'ÖV-Anbindung unter 10 Min',
'Moderner Ausbau',
'Parkplätze vorhanden',
],
weightedPreferences: [
{ criterion: 'Standortqualität Luzern', weight: 0.30 },
{ criterion: 'Ausbaustandard', weight: 0.25 },
{ criterion: 'Parkplatzverfügbarkeit', weight: 0.20 },
{ criterion: 'Erweiterungspotenzial', weight: 0.15 },
{ criterion: 'ESG', weight: 0.10 },
],
aiSummary: 'Beratungsgesellschaft sucht modernes Büro in Luzern. Gute Balance zwischen Lage, Qualität und Budget. Solides Matchprofil.',
publicVisibility: true,
status: 'public',
createdAt: '2026-04-10T14:00:00Z',
},
// ─── Original lneed-006 ────────────────────────────────────────────────────
{ {
id: 'lneed-006', id: 'lneed-006',
title: 'Pop-up-Retailfläche temporär 6 Monate', title: 'Pop-up-Retailfläche temporär 6 Monate',
+119
View File
@@ -1957,4 +1957,123 @@ export const mockMatches: Match[] = [
createdAt: '2026-01-15T09:00:00Z', createdAt: '2026-01-15T09:00:00Z',
updatedAt: '2026-01-15T09:00:00Z', updatedAt: '2026-01-15T09:00:00Z',
}, },
// ───────────────────────────────────────────────────────────────────────────
// Direct listings — Strong matches (>80%) ensuring all 6 have ≥1 STRONG
// ───────────────────────────────────────────────────────────────────────────
{
id: 'match-072',
propertyId: 'prop-d02',
needId: 'need-012',
matchScore: 85,
matchStrength: MatchStrength.STRONG,
status: MatchStatus.PENDING_REVIEW,
scoreBreakdown: { hardMatchScore: 88, softFactorScore: 84, confidenceModifier: 0.90, dataQualityModifier: 0.87, totalScore: 85 },
positiveFactors: [
{ criterion: 'Standort', weight: 0.20, score: 98, contribution: 19.6, explanation: 'Pratteln — explizit bevorzugte Region für Gewerbe Solutions AG' },
{ criterion: 'Budget', weight: 0.18, score: 100, contribution: 18.0, explanation: 'CHF 145/m²/J deutlich unter Maximum CHF 168/m²/J' },
{ criterion: 'Fläche', weight: 0.22, score: 90, contribution: 19.8, explanation: '580m² liegt ideal im Zielkorridor (400750m²)' },
{ criterion: 'Lkw-Zufahrt', weight: 0.10, score: 100, contribution: 10.0, explanation: 'Lkw-Zufahrt vorhanden — Pflichtkriterium erfüllt' },
],
negativeFactors: [
{ criterion: 'Hallenhöhe', weight: 0.08, score: 70, contribution: 5.6, explanation: 'Angabe Hallenhöhe nicht vollständig dokumentiert' },
],
tradeoffs: [
{ criterion: 'Hallenhöhe', concern: 'Angabe Hallenhöhe unvollständig — Besichtigung nötig', severity: 'LOW', mitigation: 'Vor Mietabschluss Vor-Ort-Prüfung empfohlen' },
],
explainabilitySummary: 'Ausgezeichneter Standort und Preis-Leistungs-Verhältnis für Gewerbe Solutions AG. Pratteln ist die Wunschregion, Fläche und Budget passen sehr gut.',
confidenceLevel: 0.90,
riskLevel: RiskLevel.LOW,
uncertaintyIndicators: [],
organizationId: 'org-wincasa',
createdAt: '2026-05-10T08:00:00Z',
updatedAt: '2026-05-10T08:00:00Z',
},
{
id: 'match-073',
propertyId: 'prop-d03',
needId: 'need-013',
matchScore: 84,
matchStrength: MatchStrength.STRONG,
status: MatchStatus.PENDING_REVIEW,
scoreBreakdown: { hardMatchScore: 87, softFactorScore: 83, confidenceModifier: 0.88, dataQualityModifier: 0.85, totalScore: 84 },
positiveFactors: [
{ criterion: 'Standort', weight: 0.25, score: 96, contribution: 24.0, explanation: 'Bern Bollwerk — perfekter Fit mit Bern Innenstadt / Bahnhof-Nähe' },
{ criterion: 'Budget', weight: 0.18, score: 100, contribution: 18.0, explanation: 'CHF 295/m²/J unter Maximum CHF 360/m²/J' },
{ criterion: 'Fläche', weight: 0.18, score: 92, contribution: 16.6, explanation: '240m² liegt gut im Korridor (180320m²)' },
{ criterion: 'ÖV', weight: 0.09, score: 100, contribution: 9.0, explanation: 'Hauptbahnhof Bern < 5 Min zu Fuss' },
],
negativeFactors: [
{ criterion: 'Konferenzraum', weight: 0.06, score: 60, contribution: 3.6, explanation: 'Konferenzraumsituation nicht explizit dokumentiert' },
],
tradeoffs: [],
explainabilitySummary: 'Starke Übereinstimmung: Bern Bollwerk trifft genau die Anforderungen von Bern Consulting AG — Lage, Preis und Fläche passen.',
confidenceLevel: 0.88,
riskLevel: RiskLevel.LOW,
uncertaintyIndicators: [],
organizationId: 'org-wincasa',
createdAt: '2026-05-10T09:00:00Z',
updatedAt: '2026-05-10T09:00:00Z',
},
{
id: 'match-074',
propertyId: 'prop-d04',
needId: 'need-014',
matchScore: 82,
matchStrength: MatchStrength.STRONG,
status: MatchStatus.PENDING_REVIEW,
scoreBreakdown: { hardMatchScore: 86, softFactorScore: 80, confidenceModifier: 0.84, dataQualityModifier: 0.82, totalScore: 82 },
positiveFactors: [
{ criterion: 'Standort', weight: 0.22, score: 97, contribution: 21.3, explanation: 'Winterthur Stadthausstrasse — Wunschlage für Winterthur Handel GmbH' },
{ criterion: 'Schaufensterfront', weight: 0.12, score: 100, contribution: 12.0, explanation: 'Schaufensterfront vorhanden — Pflichtkriterium erfüllt' },
{ criterion: 'Erdgeschoss', weight: 0.12, score: 100, contribution: 12.0, explanation: 'EG-Lage wie gefordert' },
{ criterion: 'Budget', weight: 0.14, score: 100, contribution: 14.0, explanation: 'CHF 360/m²/J deutlich unter Maximum CHF 480/m²/J' },
],
negativeFactors: [
{ criterion: 'Fläche', weight: 0.12, score: 68, contribution: 8.2, explanation: '185m² leicht unter Minimum 150m², aber am unteren Rand' },
{ criterion: 'Inaktiv', weight: 0.04, score: 0, contribution: 0, explanation: 'Inserat derzeit inaktiv — Verfügbarkeit zu klären' },
],
tradeoffs: [
{ criterion: 'Status', concern: 'Inserat inaktiv — Verfügbarkeit muss bestätigt werden', severity: 'LOW', mitigation: 'Direkter Kontakt mit Eigentümer empfohlen' },
],
explainabilitySummary: 'Winterthur Handel GmbH findet hier genau die gesuchte Retailfläche am richtigen Ort. Status zu klären.',
confidenceLevel: 0.84,
riskLevel: RiskLevel.LOW,
uncertaintyIndicators: ['Inserat inaktiv'],
organizationId: 'org-wincasa',
createdAt: '2026-05-10T10:00:00Z',
updatedAt: '2026-05-10T10:00:00Z',
},
{
id: 'match-075',
propertyId: 'prop-d06',
needId: 'need-015',
matchScore: 83,
matchStrength: MatchStrength.STRONG,
status: MatchStatus.PENDING_REVIEW,
scoreBreakdown: { hardMatchScore: 86, softFactorScore: 82, confidenceModifier: 0.86, dataQualityModifier: 0.84, totalScore: 83 },
positiveFactors: [
{ criterion: 'Standort', weight: 0.22, score: 95, contribution: 20.9, explanation: 'Luzern Zürichstrasse — Wunschlage für Luzern Advisory GmbH' },
{ criterion: 'Fläche', weight: 0.19, score: 96, contribution: 18.2, explanation: '450m² liegt ideal im Korridor (350600m²)' },
{ criterion: 'Budget', weight: 0.18, score: 100, contribution: 18.0, explanation: 'CHF 260/m²/J klar unter Maximum CHF 312/m²/J' },
{ criterion: 'ÖV', weight: 0.08, score: 88, contribution: 7.0, explanation: 'Gute S-Bahn-Anbindung Luzern Zentrum' },
],
negativeFactors: [
{ criterion: 'Inaktiv', weight: 0.04, score: 0, contribution: 0, explanation: 'Inserat derzeit inaktiv — Bezugsdatum offen' },
],
tradeoffs: [
{ criterion: 'Status', concern: 'Inserat inaktiv — Aktivierung und Verfügbarkeit zu klären', severity: 'LOW', mitigation: 'Direkte Anfrage an Eigentümer empfohlen' },
],
explainabilitySummary: 'Sehr guter Match für Luzern Advisory GmbH: Lage, Fläche und Budget passen ausgezeichnet. Inserat-Status zu klären.',
confidenceLevel: 0.86,
riskLevel: RiskLevel.LOW,
uncertaintyIndicators: ['Inserat inaktiv'],
organizationId: 'org-wincasa',
createdAt: '2026-05-10T11:00:00Z',
updatedAt: '2026-05-10T11:00:00Z',
},
] ]
+136
View File
@@ -437,6 +437,142 @@ export const mockNeeds: Need[] = [
updatedAt: '2025-05-22T10:00:00Z', updatedAt: '2025-05-22T10:00:00Z',
}, },
// --- need-012: Gewerbe Solutions AG — LIGHT_INDUSTRIAL Pratteln ---
{
id: 'need-012',
companyName: 'Gewerbe Solutions AG',
contactName: 'Andreas Weber',
assetType: AssetType.LIGHT_INDUSTRIAL,
requiredArea: { min: 400, max: 750 },
preferredLocations: ['Pratteln', 'Muttenz', 'Schweizerhalle', 'Liestal'],
excludedLocations: [],
budgetRange: { maxPerSqm: 168, currency: 'CHF' },
timing: {
earliestMoveIn: '2026-04-01',
latestMoveIn: '2026-10-01',
contractDurationMonths: 48,
flexibleTiming: true,
},
mustCriteriaText: ['Lkw-Zufahrt', 'Hallenhöhe min 5m', 'Drehstrom vorhanden'],
softFactors: {
requireParking: true,
},
weightingProfile: {
area: 0.22, location: 0.20, budget: 0.18, timing: 0.12,
prestige: 0.02, accessibility: 0.10, expansionPotential: 0.06, flexibility: 0.03,
visibility: 0.01, footfall: 0.00, talentAccess: 0.03, esg: 0.02, taxEnvironment: 0.01,
},
confidenceInCriteria: 0.90,
extractedFromText: 'Gewerbe-/Lagerfläche 450700m² in Pratteln oder Muttenz, Lkw-Zufahrt, max. CHF 168/m²/J.',
organizationId: 'org-wincasa',
createdAt: '2026-03-01T09:00:00Z',
updatedAt: '2026-03-01T09:00:00Z',
},
// --- need-013: Bern Consulting AG — OFFICE Bern ---
{
id: 'need-013',
companyName: 'Bern Consulting AG',
contactName: 'Sabine Gerber',
assetType: AssetType.OFFICE,
requiredArea: { min: 180, max: 320 },
preferredLocations: ['Bern', 'Bern Bollwerk', 'Bern Bahnhof', 'Bern Innenstadt'],
excludedLocations: [],
budgetRange: { maxPerSqm: 360, maxMonthlyTotal: 8500, currency: 'CHF' },
timing: {
earliestMoveIn: '2026-05-01',
latestMoveIn: '2026-11-01',
contractDurationMonths: 48,
flexibleTiming: true,
},
mustCriteriaText: ['ÖV-Anbindung < 5 Min', 'Repräsentative Lage', 'Konferenzraum vorhanden'],
softFactors: {
minPrestige: 65,
minAccessibility: 80,
requireParking: false,
maxPublicTransportMinutes: 5,
},
weightingProfile: {
area: 0.18, location: 0.25, budget: 0.18, timing: 0.11,
prestige: 0.08, accessibility: 0.09, expansionPotential: 0.02, flexibility: 0.03,
visibility: 0.02, footfall: 0.01, talentAccess: 0.05, esg: 0.02, taxEnvironment: 0.00,
},
confidenceInCriteria: 0.88,
extractedFromText: 'Beratungsunternehmen sucht Bürofläche 200300m² in Bern Zentrum nahe Bahnhof, max. CHF 360/m²/J.',
organizationId: 'org-wincasa',
createdAt: '2026-04-05T10:00:00Z',
updatedAt: '2026-04-05T10:00:00Z',
},
// --- need-014: Winterthur Handel GmbH — RETAIL Winterthur ---
{
id: 'need-014',
companyName: 'Winterthur Handel GmbH',
contactName: 'Pascal Brunner',
assetType: AssetType.RETAIL,
requiredArea: { min: 150, max: 250 },
preferredLocations: ['Winterthur', 'Winterthur Zentrum', 'Winterthur Stadthausstrasse', 'Winterthur Altstadt'],
excludedLocations: [],
budgetRange: { maxPerSqm: 480, maxMonthlyTotal: 8000, currency: 'CHF' },
timing: {
earliestMoveIn: '2026-06-01',
latestMoveIn: '2026-12-01',
contractDurationMonths: 36,
flexibleTiming: false,
},
mustCriteriaText: ['Schaufensterfront', 'Erdgeschoss', 'Gute Fussgängerfrequenz'],
softFactors: {
minPrestige: 70,
requireParking: false,
maxPublicTransportMinutes: 8,
},
weightingProfile: {
area: 0.12, location: 0.22, budget: 0.14, timing: 0.10,
prestige: 0.06, accessibility: 0.05, expansionPotential: 0.02, flexibility: 0.03,
visibility: 0.12, footfall: 0.12, talentAccess: 0.01, esg: 0.01, taxEnvironment: 0.00,
},
confidenceInCriteria: 0.87,
extractedFromText: 'Handelsbetrieb sucht Verkaufsfläche 160230m² in Winterthur Zentrum, Schaufenster, max. CHF 480/m²/J.',
organizationId: 'org-wincasa',
createdAt: '2026-03-20T11:00:00Z',
updatedAt: '2026-03-20T11:00:00Z',
},
// --- need-015: Luzern Advisory GmbH — OFFICE Luzern ---
{
id: 'need-015',
companyName: 'Luzern Advisory GmbH',
contactName: 'Markus Bucher',
assetType: AssetType.OFFICE,
requiredArea: { min: 350, max: 600 },
preferredLocations: ['Luzern', 'Luzern Zentrum', 'Luzern-West', 'Emmen'],
excludedLocations: [],
budgetRange: { maxPerSqm: 312, maxMonthlyTotal: 14000, currency: 'CHF' },
timing: {
earliestMoveIn: '2026-06-01',
latestMoveIn: '2027-01-01',
contractDurationMonths: 60,
flexibleTiming: true,
},
mustCriteriaText: ['ÖV < 10 Min', 'Moderner Ausbau', 'Parkplätze vorhanden'],
softFactors: {
minPrestige: 60,
minAccessibility: 70,
requireParking: true,
maxPublicTransportMinutes: 10,
},
weightingProfile: {
area: 0.19, location: 0.22, budget: 0.18, timing: 0.11,
prestige: 0.07, accessibility: 0.08, expansionPotential: 0.03, flexibility: 0.04,
visibility: 0.02, footfall: 0.01, talentAccess: 0.04, esg: 0.02, taxEnvironment: 0.01,
},
confidenceInCriteria: 0.86,
extractedFromText: 'Beratungsgesellschaft sucht moderne Bürofläche 400550m² in Luzern, max. CHF 312/m²/J, Parkplätze nötig.',
organizationId: 'org-wincasa',
createdAt: '2026-04-10T14:00:00Z',
updatedAt: '2026-04-10T14:00:00Z',
},
// --- need-uc-c: Renato's Vermögensverwalter — Use Case C --- // --- need-uc-c: Renato's Vermögensverwalter — Use Case C ---
{ {
id: 'need-uc-c', id: 'need-uc-c',
+186 -4
View File
@@ -16,11 +16,15 @@ import {
Tooltip, Tooltip,
Typography, Typography,
} from '@mui/material' } from '@mui/material'
import { Plus, Trash2 } from 'lucide-react' import { ArrowLeft, Plus, Trash2 } from 'lucide-react'
import { useProperties, useUpdateProperty, useRemoveProperty } from '../../hooks/useProperties' import { useProperties, useUpdateProperty, useRemoveProperty } from '../../hooks/useProperties'
import { useMatchesByProperty } from '../../hooks/useMatches' import { useMatchesByProperty } from '../../hooks/useMatches'
import { useLatentNeedById } from '../../hooks/useLatentNeeds'
import { PropertyDetailView } from '../../components/supply' import { PropertyDetailView } from '../../components/supply'
import { PublicNeedDetail, OfferWizard } from '../../components/anfragencenter'
import { NEED_TO_LATENT_NEED } from '../../lib/needToLatentNeedMap'
import type { Property } from '../../domain/property' import type { Property } from '../../domain/property'
import type { Match } from '../../domain/match'
const ASSET_LABELS: Record<string, string> = { const ASSET_LABELS: Record<string, string> = {
OFFICE: 'Büro', OFFICE: 'Büro',
@@ -44,17 +48,158 @@ function matchColor(score: number): string {
return '#dc2626' return '#dc2626'
} }
// ─── Suchabo dialog ────────────────────────────────────────────────────────
function MatchListItem({
match,
onSelect,
}: {
match: Match
onSelect: (needId: string) => void
}) {
const latentNeedId = NEED_TO_LATENT_NEED[match.needId] ?? null
const { data: latentNeed } = useLatentNeedById(latentNeedId)
const companyName = latentNeed?.tenantCompany ?? match.needId
return (
<Box
sx={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
px: 2, py: 1.5,
border: '1px solid #e2e8f0',
borderRadius: 1.5,
bgcolor: 'white',
gap: 1,
}}
>
<Box sx={{ minWidth: 0 }}>
<Typography variant="body2" sx={{ fontWeight: 600, color: '#0f172a', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
{companyName}
</Typography>
{latentNeed && (
<Typography variant="caption" sx={{ color: '#64748b' }}>
{latentNeed.desiredLocation}
</Typography>
)}
</Box>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, flexShrink: 0 }}>
<Chip
label={`${match.matchScore}%`}
size="small"
sx={{
height: 20,
fontSize: '0.65rem',
fontWeight: 700,
bgcolor: `${matchColor(match.matchScore)}18`,
color: matchColor(match.matchScore),
}}
/>
{latentNeedId ? (
<Button
size="small"
variant="contained"
onClick={() => onSelect(match.needId)}
sx={{
textTransform: 'none',
fontSize: '0.75rem',
bgcolor: '#1e3a5f',
'&:hover': { bgcolor: '#162d4a' },
}}
>
Anschreiben
</Button>
) : (
<Tooltip title="Kein öffentliches Suchabo verknüpft">
<span>
<Button size="small" disabled sx={{ textTransform: 'none', fontSize: '0.75rem' }}>
Anschreiben
</Button>
</span>
</Tooltip>
)}
</Box>
</Box>
)
}
interface SuchaboDialogProps {
propertyId: string
matches: Match[]
onClose: () => void
}
function SuchaboDialogContent({ matches }: SuchaboDialogProps) {
const [selectedNeedId, setSelectedNeedId] = useState<string | null>(
matches.length === 1 ? matches[0].needId : null,
)
const latentNeedId = selectedNeedId ? (NEED_TO_LATENT_NEED[selectedNeedId] ?? null) : null
const { data: latentNeed, isLoading } = useLatentNeedById(latentNeedId)
const showList = !selectedNeedId
if (showList) {
return (
<Box sx={{ p: 2.5, display: 'flex', flexDirection: 'column', gap: 1 }}>
{matches.map(m => (
<MatchListItem key={m.id} match={m} onSelect={setSelectedNeedId} />
))}
</Box>
)
}
if (isLoading) {
return (
<Box sx={{ display: 'flex', justifyContent: 'center', py: 6 }}>
<CircularProgress size={28} />
</Box>
)
}
if (!latentNeed) {
return (
<Box sx={{ p: 3, textAlign: 'center' }}>
<Typography variant="body2" color="text.secondary">Suchabo nicht gefunden.</Typography>
</Box>
)
}
return (
<Box>
{matches.length > 1 && (
<Box sx={{ px: 2, pt: 1.5 }}>
<Button
startIcon={<ArrowLeft size={14} />}
size="small"
onClick={() => setSelectedNeedId(null)}
sx={{ textTransform: 'none', color: '#64748b', fontWeight: 500 }}
>
Alle Suchabos
</Button>
</Box>
)}
<PublicNeedDetail need={latentNeed} />
</Box>
)
}
// ─── Row ───────────────────────────────────────────────────────────────────
interface RowProps { interface RowProps {
p: Property p: Property
isLast: boolean isLast: boolean
onDetail: (id: string) => void onDetail: (id: string) => void
onToggleStatus: (p: Property) => void onToggleStatus: (p: Property) => void
onDelete: (p: Property) => void onDelete: (p: Property) => void
onChipClick: (propertyId: string, matches: Match[]) => void
updatePending: boolean updatePending: boolean
removePending: boolean removePending: boolean
} }
const ListingRow = memo(function ListingRow({ p, isLast, onDetail, onToggleStatus, onDelete, updatePending, removePending }: RowProps) { const ListingRow = memo(function ListingRow({
p, isLast, onDetail, onToggleStatus, onDelete, onChipClick, updatePending, removePending,
}: RowProps) {
const { data: matches = [] } = useMatchesByProperty(p.id) const { data: matches = [] } = useMatchesByProperty(p.id)
const topScore = matches.length > 0 ? Math.max(...matches.map(m => m.matchScore)) : 0 const topScore = matches.length > 0 ? Math.max(...matches.map(m => m.matchScore)) : 0
@@ -102,17 +247,19 @@ const ListingRow = memo(function ListingRow({ p, isLast, onDetail, onToggleStatu
{/* Match count */} {/* Match count */}
<Box onClick={e => e.stopPropagation()}> <Box onClick={e => e.stopPropagation()}>
{matches.length > 0 ? ( {matches.length > 0 ? (
<Tooltip title={matches.map(m => `${m.matchScore}% · need-${m.needId.slice(-3)}`).join('\n')}> <Tooltip title="Suchabos anzeigen und anschreiben">
<Chip <Chip
label={`${matches.length} Suchabo${matches.length !== 1 ? 's' : ''}`} label={`${matches.length} Suchabo${matches.length !== 1 ? 's' : ''}`}
size="small" size="small"
onClick={() => onChipClick(p.id, matches)}
sx={{ sx={{
height: 20, height: 20,
fontSize: '0.65rem', fontSize: '0.65rem',
fontWeight: 600, fontWeight: 600,
bgcolor: `${matchColor(topScore)}18`, bgcolor: `${matchColor(topScore)}18`,
color: matchColor(topScore), color: matchColor(topScore),
cursor: 'default', cursor: 'pointer',
'&:hover': { opacity: 0.85 },
}} }}
/> />
</Tooltip> </Tooltip>
@@ -156,6 +303,8 @@ const ListingRow = memo(function ListingRow({ p, isLast, onDetail, onToggleStatu
) )
}) })
// ─── Page ─────────────────────────────────────────────────────────────────
export default function MyListings() { export default function MyListings() {
const navigate = useNavigate() const navigate = useNavigate()
@@ -166,6 +315,10 @@ export default function MyListings() {
const [detailId, setDetailId] = useState<string | null>(null) const [detailId, setDetailId] = useState<string | null>(null)
const [confirmDelete, setConfirmDelete] = useState<Property | null>(null) const [confirmDelete, setConfirmDelete] = useState<Property | null>(null)
const [actionError, setActionError] = useState<string | null>(null) const [actionError, setActionError] = useState<string | null>(null)
const [suchaboDialog, setSuchaboDialog] = useState<{
propertyId: string
matches: Match[]
} | null>(null)
function handleToggleStatus(p: Property) { function handleToggleStatus(p: Property) {
const next = p.status === 'ACTIVE' ? 'INACTIVE' : 'ACTIVE' const next = p.status === 'ACTIVE' ? 'INACTIVE' : 'ACTIVE'
@@ -254,6 +407,7 @@ export default function MyListings() {
onDetail={setDetailId} onDetail={setDetailId}
onToggleStatus={handleToggleStatus} onToggleStatus={handleToggleStatus}
onDelete={setConfirmDelete} onDelete={setConfirmDelete}
onChipClick={(pid, m) => setSuchaboDialog({ propertyId: pid, matches: m })}
updatePending={updateProperty.isPending} updatePending={updateProperty.isPending}
removePending={removeProperty.isPending} removePending={removeProperty.isPending}
/> />
@@ -273,6 +427,31 @@ export default function MyListings() {
)} )}
</Drawer> </Drawer>
{/* Suchabo dialog */}
<Dialog
open={!!suchaboDialog}
onClose={() => setSuchaboDialog(null)}
maxWidth="sm"
fullWidth
slotProps={{ paper: { sx: { maxHeight: '85vh' } } }}
>
<DialogTitle sx={{ fontWeight: 700, fontSize: '1rem', pb: 1 }}>
Suchabos
{suchaboDialog && (
<Typography variant="body2" color="text.secondary" sx={{ fontWeight: 400 }}>
{suchaboDialog.matches.length} Suchabo{suchaboDialog.matches.length !== 1 ? 's' : ''} matchen dieses Inserat
</Typography>
)}
</DialogTitle>
{suchaboDialog && (
<SuchaboDialogContent
propertyId={suchaboDialog.propertyId}
matches={suchaboDialog.matches}
onClose={() => setSuchaboDialog(null)}
/>
)}
</Dialog>
{/* Delete confirmation */} {/* Delete confirmation */}
<Dialog open={!!confirmDelete} onClose={() => setConfirmDelete(null)} maxWidth="xs" fullWidth> <Dialog open={!!confirmDelete} onClose={() => setConfirmDelete(null)} maxWidth="xs" fullWidth>
<DialogTitle>Inserat löschen?</DialogTitle> <DialogTitle>Inserat löschen?</DialogTitle>
@@ -295,6 +474,9 @@ export default function MyListings() {
</Button> </Button>
</DialogActions> </DialogActions>
</Dialog> </Dialog>
{/* OfferWizard — triggered by PublicNeedDetail's "Angebot erstellen" button */}
<OfferWizard />
</Box> </Box>
) )
} }