diff --git a/src/App.tsx b/src/App.tsx
index 9832131..2f94c41 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -27,6 +27,7 @@ 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 NewListing = lazy(() => import('./pages/supply/NewListing'))
const AISearch = lazy(() => import('./pages/demand/AISearch'))
const Results = lazy(() => import('./pages/demand/Results'))
@@ -62,6 +63,7 @@ function App() {
} />
} />
} />
+ } />
{/* Demand Workspace */}
diff --git a/src/components/layout/AppShell.tsx b/src/components/layout/AppShell.tsx
index e07056b..8307067 100644
--- a/src/components/layout/AppShell.tsx
+++ b/src/components/layout/AppShell.tsx
@@ -39,6 +39,7 @@ import {
Menu,
Kanban,
BellRing,
+ Plus,
} from 'lucide-react'
import type { LucideIcon } from 'lucide-react'
import { OrganizationContextBadge } from './OrganizationContextBadge'
@@ -88,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/new-listing', label: 'Neues Inserat', icon: Plus },
],
},
[WorkspaceType.DEMAND]: {
diff --git a/src/components/match-detail/SourceProvenancePanel.tsx b/src/components/match-detail/SourceProvenancePanel.tsx
index 10c6718..aa0380d 100644
--- a/src/components/match-detail/SourceProvenancePanel.tsx
+++ b/src/components/match-detail/SourceProvenancePanel.tsx
@@ -13,6 +13,7 @@ interface Props {
export function SourceProvenancePanel({ property }: Props) {
if (!property) return null
+ if (property.sourceType === 'DIRECT' || property.sourceMeta?.sourceType === 'DIRECT') return null
const freshness = property.dataQuality?.freshness
const freshnessMeta = freshness ? (FRESHNESS_META[freshness] ?? null) : null
diff --git a/src/components/supply/PropertyDetailView.tsx b/src/components/supply/PropertyDetailView.tsx
index 1ea10ca..df57862 100644
--- a/src/components/supply/PropertyDetailView.tsx
+++ b/src/components/supply/PropertyDetailView.tsx
@@ -106,6 +106,7 @@ function MatchPill({ m }: { m: UnitNeedMatch }) {
}
function UnitStructurePanel({ p }: { p: Property }) {
+ const navigate = useNavigate()
const [selectedIds, setSelectedIds] = useState>(new Set())
const [expandedUnit, setExpandedUnit] = useState(null)
@@ -204,6 +205,28 @@ function UnitStructurePanel({ p }: { p: Property }) {
{u.isFlexible && (
)}
+
>
) : (
{u.currentTenant ?? 'Vermietet'}
diff --git a/src/pages/supply/NewListing.tsx b/src/pages/supply/NewListing.tsx
new file mode 100644
index 0000000..def9aa6
--- /dev/null
+++ b/src/pages/supply/NewListing.tsx
@@ -0,0 +1,310 @@
+import { useState } from 'react'
+import { useLocation, useNavigate } from 'react-router'
+import {
+ Alert,
+ Box,
+ Button,
+ Card,
+ CircularProgress,
+ Divider,
+ MenuItem,
+ TextField,
+ Typography,
+} from '@mui/material'
+import { ArrowLeft, CheckCircle } from 'lucide-react'
+import { AssetType, ResultType, AvailabilityStatus } from '../../domain/enums'
+import { propertyService } from '../../services/propertyService'
+import type { CreatePropertyInput } from '../../domain/property'
+
+const ASSET_TYPE_LABELS: Record = {
+ OFFICE: 'Büro',
+ RETAIL: 'Einzelhandel',
+ LIGHT_INDUSTRIAL: 'Leichtindustrie',
+ LOGISTICS: 'Logistik',
+ PRODUCTION: 'Produktion',
+ MIXED: 'Gemischt',
+}
+
+interface LocationState {
+ prefill?: {
+ assetType?: string
+ street?: string
+ houseNumber?: string
+ postalCode?: string
+ city?: string
+ areaSqm?: number
+ rentPricePerSqm?: number
+ unitLabel?: string
+ propertyId?: string
+ }
+}
+
+export default function NewListing() {
+ const navigate = useNavigate()
+ const { state } = useLocation() as { state: LocationState | null }
+ const pre = state?.prefill ?? {}
+
+ 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 [availableFrom, setAvailableFrom] = useState('')
+ const [description, setDescription] = useState('')
+ const [contactName, setContactName] = useState('')
+ const [contactEmail, setContactEmail] = useState('')
+ const [contactPhone, setContactPhone] = useState('')
+
+ const [submitting, setSubmitting] = useState(false)
+ const [error, setError] = useState(null)
+ const [created, setCreated] = useState(false)
+
+ const isPrefilled = !!pre.propertyId
+
+ function validate(): string | null {
+ if (!street.trim()) return 'Strasse erforderlich'
+ if (!postalCode.trim()) return 'PLZ erforderlich'
+ if (!city.trim()) return 'Ort erforderlich'
+ if (!areaSqm || isNaN(Number(areaSqm)) || Number(areaSqm) <= 0) return 'Gültige Fläche eingeben'
+ if (!rentPerSqm || isNaN(Number(rentPerSqm)) || Number(rentPerSqm) <= 0) return 'Gültigen Mietpreis eingeben'
+ return null
+ }
+
+ async function handleSubmit() {
+ const err = validate()
+ if (err) { setError(err); return }
+ setError(null)
+ setSubmitting(true)
+
+ const area = Number(areaSqm)
+ const rent = Number(rentPerSqm)
+
+ 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' },
+ 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,
+ dataQuality: {
+ score: 1.0,
+ missingCriticalFields: [],
+ missingOptionalFields: [],
+ freshness: 'FRESH',
+ warnings: [],
+ },
+ status: 'ACTIVE',
+ }
+
+ try {
+ await propertyService.create(input)
+ setCreated(true)
+ } catch {
+ setError('Fehler beim Erstellen des Inserats. Bitte erneut versuchen.')
+ } finally {
+ setSubmitting(false)
+ }
+ }
+
+ if (created) {
+ return (
+
+
+ Inserat erstellt
+
+ Das Inserat wurde erfolgreich veröffentlicht und ist jetzt für passende Suchanfragen sichtbar.
+
+
+
+
+
+
+ )
+ }
+
+ return (
+
+
+ }
+ onClick={() => navigate(-1)}
+ sx={{ color: '#64748b', textTransform: 'none', px: 0 }}
+ >
+ Zurück
+
+
+
+ Neues Inserat erstellen
+
+ {isPrefilled
+ ? `Einheit ${pre.unitLabel ?? ''} aus Portfolio vorausgefüllt — Angaben prüfen und veröffentlichen.`
+ : 'Fläche direkt inserieren — ohne vollständiges Objekt im Portfolio.'}
+
+
+
+ Flächendetails
+
+
+ setAssetType(e.target.value)}
+ size="small"
+ fullWidth
+ >
+ {Object.entries(ASSET_TYPE_LABELS).map(([v, l]) => (
+
+ ))}
+
+
+ setAreaSqm(e.target.value)}
+ size="small"
+ type="number"
+ inputProps={{ min: 1 }}
+ fullWidth
+ />
+
+ setRentPerSqm(e.target.value)}
+ size="small"
+ type="number"
+ inputProps={{ min: 1 }}
+ fullWidth
+ />
+
+ setAvailableFrom(e.target.value)}
+ size="small"
+ type="date"
+ slotProps={{ inputLabel: { shrink: true } }}
+ fullWidth
+ />
+
+
+ setDescription(e.target.value)}
+ size="small"
+ multiline
+ rows={3}
+ fullWidth
+ sx={{ mt: 2 }}
+ />
+
+
+
+ Adresse
+
+ setStreet(e.target.value)}
+ size="small"
+ fullWidth
+ />
+ setHouseNumber(e.target.value)}
+ size="small"
+ fullWidth
+ />
+
+
+ setPostalCode(e.target.value)}
+ size="small"
+ fullWidth
+ />
+ setCity(e.target.value)}
+ size="small"
+ fullWidth
+ />
+
+
+
+
+ Kontakt (optional)
+
+ setContactName(e.target.value)}
+ size="small"
+ fullWidth
+ />
+ setContactPhone(e.target.value)}
+ size="small"
+ fullWidth
+ />
+ setContactEmail(e.target.value)}
+ size="small"
+ type="email"
+ fullWidth
+ sx={{ gridColumn: '1 / -1' }}
+ />
+
+
+
+ {error && (
+ {error}
+ )}
+
+
+
+
+
+ : null}
+ >
+ {submitting ? 'Wird erstellt…' : 'Inserat veröffentlichen'}
+
+
+
+ )
+}