diff --git a/src/App.tsx b/src/App.tsx
index d71627e..914d7d7 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -26,7 +26,7 @@ const Anfragencenter = lazy(() => import('./pages/supply/Anfragencenter'))
const FutureAvailability = lazy(() => import('./pages/supply/FutureAvailability'))
const DataQuality = lazy(() => import('./pages/supply/DataQuality'))
const ReminderManager = lazy(() => import('./pages/supply/ReminderManager'))
-const MarketLeads = lazy(() => import('./pages/supply/MarketLeads'))
+const MarketIntelligence = lazy(() => import('./pages/supply/MarketIntelligence'))
const NewListing = lazy(() => import('./pages/supply/NewListing'))
const MyListings = lazy(() => import('./pages/supply/MyListings'))
@@ -38,7 +38,6 @@ const Anfragen = lazy(() => import('./pages/demand/Anfragen'))
const Pipeline = lazy(() => import('./pages/demand/Pipeline'))
const PropertyDetail = lazy(() => import('./pages/demand/PropertyDetail'))
-const MarketIntelligence = lazy(() => import('./pages/ops/MarketIntelligence'))
function App() {
return (
@@ -62,7 +61,6 @@ function App() {
} />
} />
} />
- } />
} />
} />
} />
diff --git a/src/components/layout/appShellConfig.ts b/src/components/layout/appShellConfig.ts
index c871d9a..ce136bb 100644
--- a/src/components/layout/appShellConfig.ts
+++ b/src/components/layout/appShellConfig.ts
@@ -3,7 +3,6 @@ import type { LucideIcon } from 'lucide-react'
import {
LayoutDashboard,
Building2,
- Target,
CheckSquare,
Search,
List,
@@ -49,10 +48,9 @@ export const WORKSPACE_CONFIG: Record = {
{ path: '/supply/dashboard', label: 'Übersicht', icon: LayoutDashboard },
{ path: '/supply/properties', label: 'Meine Objekte', icon: Building2 },
{ path: '/supply/reminder-manager', label: 'Reminder Manager', icon: BellRing },
- { path: '/supply/market-leads', label: 'Markt-Leads', icon: Target },
{ 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/market-intelligence', label: 'Marktchancen', icon: Radar },
{ path: '/supply/my-listings', label: 'Inserate', icon: ClipboardList },
],
},
diff --git a/src/pages/supply/MarketIntelligence.tsx b/src/pages/supply/MarketIntelligence.tsx
new file mode 100644
index 0000000..8c9ae3b
--- /dev/null
+++ b/src/pages/supply/MarketIntelligence.tsx
@@ -0,0 +1,289 @@
+import { memo, useState, useEffect } from 'react'
+import { useNavigate } from 'react-router'
+import { Box, Chip, Divider, Skeleton, Typography, Button } from '@mui/material'
+import { AlertCircle, Bell, Building2, CheckCircle2, Sparkles, TrendingUp } from 'lucide-react'
+import { useMarketLeads } from '../../hooks/useMarketLeads'
+import type { MarketLead } from '../../hooks/useMarketLeads'
+
+const SOURCE_LABELS: Record = {
+ JOB_POSTING: 'Stelleninserate',
+ PRESS: 'Pressebericht',
+ COMPANY_REPORT: 'Geschäftsbericht',
+ MARKET_DATA: 'Marktdaten',
+ MANUAL: 'Manuell',
+ CONSTRUCTION_PERMIT: 'Baugenehmigung',
+ LEASE_CONTRACT: 'Mietvertrag',
+}
+
+function probColor(p: number): string {
+ if (p >= 0.70) return '#1a7a4a'
+ if (p >= 0.50) return '#d97706'
+ return '#dc2626'
+}
+
+const LeadListItem = memo(function LeadListItem({
+ lead, selected, onClick,
+}: {
+ lead: MarketLead
+ selected: boolean
+ onClick: () => void
+}) {
+ const { signal, matchingProperties } = lead
+ const prob = Math.round(signal.probability * 100)
+ const color = probColor(signal.probability)
+
+ return (
+
+
+
+ {signal.companyName ?? signal.locationHint}
+
+
+
+ {signal.title && (
+
+ {signal.title}
+
+ )}
+
+
+ {signal.locationHint} · {signal.timeHorizonMonths} Mo.
+
+ {matchingProperties.length > 0 && (
+
+ )}
+
+
+ )
+})
+
+function LeadDetail({ lead }: { lead: MarketLead }) {
+ const navigate = useNavigate()
+ const { signal, matchingProperties } = lead
+ const prob = Math.round(signal.probability * 100)
+ const color = probColor(signal.probability)
+ const sourceLabel = SOURCE_LABELS[signal.source.type] ?? signal.source.type
+
+ return (
+
+ {/* Header */}
+
+
+ {signal.companyName ?? signal.locationHint}
+
+ {signal.title && (
+
+ {signal.title}
+
+ )}
+
+
+
+ {signal.areaSqmEstimate && (
+
+ )}
+
+
+
+
+
+
+ {/* AI summary */}
+ {(signal.aiSummary ?? signal.strategicInterpretation) && (
+
+
+
+
+ KI-Analyse
+
+
+
+ {signal.aiSummary ?? signal.strategicInterpretation}
+
+
+ )}
+
+ {/* Market indicators */}
+ {(signal.marketIndicators?.length ?? 0) > 0 && (
+
+
+ Erkannte Nachfragesignale
+
+
+ {signal.marketIndicators!.map((ind, i) => (
+
+
+ {ind}
+
+ ))}
+
+
+ )}
+
+ {/* Confirmed / unconfirmed facts */}
+ {((signal.confirmedFacts?.length ?? 0) + (signal.unconfirmedFacts?.length ?? 0)) > 0 && (
+
+
+ Faktencheck
+
+
+ {(signal.confirmedFacts ?? []).map((f, i) => (
+
+
+ {f}
+
+ ))}
+ {(signal.unconfirmedFacts ?? []).map((f, i) => (
+
+
+ {f}
+
+ ))}
+
+
+ )}
+
+
+
+ {/* Portfolio matches */}
+
+
+ Passende Objekte im Portfolio ({matchingProperties.length})
+
+ {matchingProperties.length === 0 ? (
+
+ Kein passendes Objekt gefunden — manuelle Prüfung empfohlen
+
+ ) : (
+
+ {matchingProperties.slice(0, 3).map(p => (
+ navigate('/supply/properties')}
+ sx={{
+ display: 'flex', alignItems: 'center', gap: 1,
+ px: 1.25, py: 0.875, borderRadius: 1,
+ bgcolor: '#f0fdf4', border: '1px solid #bbf7d0',
+ cursor: 'pointer', '&:hover': { bgcolor: '#dcfce7' },
+ }}
+ >
+
+
+ {p.title}
+
+
+ {p.areaSqm.toLocaleString('de-CH')} m²
+
+
+ ))}
+ {matchingProperties.length > 3 && (
+
+ +{matchingProperties.length - 3} weitere Objekte
+
+ )}
+
+ )}
+
+
+ {/* Action */}
+
+ }
+ onClick={() => navigate('/supply/reminder-manager')}
+ sx={{ textTransform: 'none', fontSize: '0.8rem' }}
+ >
+ Reminder erstellen
+
+
+
+ {signal.disclaimer && (
+
+ {signal.disclaimer}
+
+ )}
+
+
+ )
+}
+
+export default function MarketIntelligence() {
+ const [selectedId, setSelectedId] = useState(null)
+ const { data: leads, isLoading } = useMarketLeads()
+
+ useEffect(() => {
+ if (!selectedId && leads.length > 0) setSelectedId(leads[0].signal.id)
+ }, [leads, selectedId])
+
+ const selectedLead = leads.find(l => l.signal.id === selectedId) ?? null
+
+ return (
+
+
+ Marktchancen
+
+ Erkannte Unternehmen mit aktivem Flächenbedarf — potenzielle Mieter für Ihr Portfolio
+
+
+
+
+ {/* Left pane */}
+
+
+ Markt-Signale
+
+
+
+ {isLoading
+ ? [1, 2, 3, 4].map(i => (
+
+
+
+
+
+ ))
+ : leads.map(lead => (
+ setSelectedId(lead.signal.id)}
+ />
+ ))}
+
+
+
+ {/* Right pane */}
+
+ {selectedLead ? (
+
+ ) : (
+
+ Signal aus der Liste auswählen
+
+ )}
+
+
+
+ )
+}
diff --git a/src/pages/supply/MarketLeads.tsx b/src/pages/supply/MarketLeads.tsx
deleted file mode 100644
index f1690f8..0000000
--- a/src/pages/supply/MarketLeads.tsx
+++ /dev/null
@@ -1,296 +0,0 @@
-import {
- Box,
- Chip,
- Divider,
- Paper,
- Skeleton,
- Typography,
-} from '@mui/material'
-import {
- AlertCircle,
- BarChart2,
- Briefcase,
- Building2,
- CheckCircle2,
- FileText,
- Newspaper,
- Target,
- TrendingUp,
- Users,
-} from 'lucide-react'
-import { useNavigate } from 'react-router'
-import { useMarketLeads } from '../../hooks/useMarketLeads'
-import { DS_TEXT, DS_BG, DS_SURFACE, DS_BORDER, DS_MARKET_SIGNAL } from '../../lib/ds'
-import { DecisionContextPanel } from '../../components/ui'
-import type { MarketLead } from '../../hooks/useMarketLeads'
-import type { Property } from '../../domain/property'
-
-// ── Source meta ──────────────────────────────────────────────────────────────
-
-const SOURCE_META: Record = {
- JOB_POSTING: { label: 'Stelleninserate', icon: },
- PRESS: { label: 'Pressebericht', icon: },
- COMPANY_REPORT: { label: 'Geschäftsbericht',icon: },
- MARKET_DATA: { label: 'Marktdaten', icon: },
-}
-
-const QUALITY_META: Record = {
- HIGH: { label: 'Hohe Signalqualität', color: DS_TEXT.success },
- MEDIUM: { label: 'Mittlere Signalqualität', color: DS_TEXT.warning },
- LOW: { label: 'Niedrige Signalqualität', color: DS_TEXT.error },
-}
-
-function signalQuality(probability: number): 'HIGH' | 'MEDIUM' | 'LOW' {
- return probability >= 0.70 ? 'HIGH' : probability >= 0.50 ? 'MEDIUM' : 'LOW'
-}
-
-// ── Property match chip ───────────────────────────────────────────────────────
-
-function PropertyMatchRow({ p }: { p: Property }) {
- const navigate = useNavigate()
- return (
- navigate('/supply/properties')}
- sx={{
- display: 'flex', alignItems: 'center', gap: 1,
- px: 1.25, py: 0.75, borderRadius: 1,
- bgcolor: DS_SURFACE.success.bg, border: `1px solid ${DS_SURFACE.success.border}`,
- cursor: 'pointer', '&:hover': { bgcolor: DS_SURFACE.success.bg },
- }}
- >
-
-
- {p.title}
-
-
- {p.areaSqm.toLocaleString('de-CH')} m²
-
-
- )
-}
-
-// ── Lead card ─────────────────────────────────────────────────────────────────
-
-function LeadCard({ lead }: { lead: MarketLead }) {
- const { signal, matchingProperties } = lead
- const probPct = Math.round(signal.probability * 100)
- const qual = signalQuality(signal.probability)
- const qualMeta = QUALITY_META[qual]
- const sourceMeta = SOURCE_META[signal.source.type] ?? null
-
- return (
-
- {/* Header */}
-
-
-
-
-
-
-
- {signal.companyName ?? signal.locationHint}
-
-
- {signal.title}
-
-
-
-
-
-
- {/* Key facts strip */}
-
- {signal.areaSqmEstimate && (
-
- Flächenbedarf
- ~{signal.areaSqmEstimate.toLocaleString('de-CH')} m²
-
- )}
-
- Zeithorizont
- ~{signal.timeHorizonMonths} Monate
-
-
- Wahrscheinlichkeit
- {probPct}%
-
-
- Standort
- {signal.locationHint}
-
- {sourceMeta && (
-
- Quelle
-
- {sourceMeta.icon}
- {sourceMeta.label}
-
-
- )}
-
-
-
-
- {/* Market indicators */}
- {signal.marketIndicators && signal.marketIndicators.length > 0 && (
-
-
- Erkannte Nachfragesignale
-
-
- {signal.marketIndicators.map((ind, i) => (
-
-
- {ind}
-
- ))}
-
-
- )}
-
- {/* Confirmed / unconfirmed facts */}
- {((signal.confirmedFacts?.length ?? 0) > 0 || (signal.unconfirmedFacts?.length ?? 0) > 0) && (
-
-
- {(signal.confirmedFacts ?? []).map((f, i) => (
-
-
- {f}
-
- ))}
- {(signal.unconfirmedFacts ?? []).map((f, i) => (
-
-
- {f}
-
- ))}
-
-
- )}
-
-
-
- {/* Portfolio matches */}
-
-
- Passende Objekte im Portfolio ({matchingProperties.length})
-
- {matchingProperties.length === 0 ? (
-
- Kein passendes Portfolioobjekt gefunden — manuell prüfen empfohlen
-
- ) : (
-
- {matchingProperties.slice(0, 3).map(p => (
-
- ))}
- {matchingProperties.length > 3 && (
-
- +{matchingProperties.length - 3} weitere Objekte
-
- )}
-
- )}
-
-
- {/* Disclaimer */}
-
- {signal.disclaimer}
-
-
- )
-}
-
-// ── Loading skeleton ──────────────────────────────────────────────────────────
-
-function LeadSkeleton() {
- return (
-
-
-
-
-
-
- )
-}
-
-// ── Page ──────────────────────────────────────────────────────────────────────
-
-export default function MarketLeads() {
- const { data: leads, isLoading } = useMarketLeads()
-
- const leadsWithMatch = leads.filter(l => l.matchingProperties.length > 0)
-
- return (
-
-
- {/* Header */}
-
-
-
-
- Markt-Leads
-
- Erkannte Unternehmen mit aktivem Flächenbedarf — potenzielle Mieter für Ihr Portfolio
-
-
- {!isLoading && (
-
- )}
-
-
-
-
-
- {/* Decision context */}
- {!isLoading && leads.length > 0 && (
- 0
- ? [{ label: 'mit passendem Objekt', value: leadsWithMatch.length, severity: 'positive' as const }]
- : []),
- ]}
- risks={leads.length - leadsWithMatch.length > 0
- ? [`${leads.length - leadsWithMatch.length} Leads ohne direkten Portfolio-Match — Leerstandsanalyse empfohlen`]
- : []
- }
- actions={[]}
- />
- )}
-
- {isLoading ? (
- <>
-
-
-
- >
- ) : leads.length === 0 ? (
-
-
- Keine aktiven Markt-Leads gefunden.
- Signale werden laufend aus öffentlichen Quellen erkannt.
-
- ) : (
- leads.map(lead => )
- )}
-
-
- )
-}