refactor: unit-centric lease view — Einheiten & Mietverträge
Replace building-level Miet- & Mieterinformationen section with unit-level lease display. Each unit row now shows tenant name and lease end date directly; expand reveals full contract details (Mietbeginn, Mietende, Laufzeit, Breakout, source system, doc link). Floor grouping groups consecutive same-floor units under one label. Domain model extended with Lease and Tenant types; deprecated property-level tenant fields preserved via @deprecated for compat. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -63,45 +63,19 @@ export function PropertyDetailOverview({ p, editing, draft, onDraftChange }: Pro
|
||||
</Box>
|
||||
) : null}
|
||||
|
||||
{/* Lease & Tenant */}
|
||||
<SectionTitle title="Miet- & Mieterinformationen" />
|
||||
{editing ? (
|
||||
{/* Floor / unit structure — primary tenant/lease view */}
|
||||
<UnitStructurePanel p={p} />
|
||||
|
||||
{/* Fallback: property-level contract doc only when no unit-level contracts exist */}
|
||||
{(() => {
|
||||
const hasUnitContracts = p.units?.some(u => u.leases?.some(l => l.contractDocumentUrl))
|
||||
if (editing) {
|
||||
return (
|
||||
<Box sx={{ mb: 2 }}>
|
||||
<Box sx={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 1.5, mb: 1 }}>
|
||||
<TextField
|
||||
label="Aktueller Mieter"
|
||||
size="small"
|
||||
value={draft.currentTenant ?? p.currentTenant ?? ''}
|
||||
onChange={e => onDraftChange({ ...draft, currentTenant: e.target.value })}
|
||||
/>
|
||||
<TextField
|
||||
label="Mietlaufzeit"
|
||||
size="small"
|
||||
value={draft.leaseTerm ?? p.leaseTerm ?? ''}
|
||||
onChange={e => onDraftChange({ ...draft, leaseTerm: e.target.value })}
|
||||
placeholder="z.B. 5 Jahre"
|
||||
/>
|
||||
<TextField
|
||||
label="Mietbeginn"
|
||||
size="small"
|
||||
type="date"
|
||||
slotProps={{ inputLabel: { shrink: true } }}
|
||||
value={draft.leaseStartDate ?? p.leaseStartDate ?? ''}
|
||||
onChange={e => onDraftChange({ ...draft, leaseStartDate: e.target.value })}
|
||||
/>
|
||||
<TextField
|
||||
label="Mietende"
|
||||
size="small"
|
||||
type="date"
|
||||
slotProps={{ inputLabel: { shrink: true } }}
|
||||
value={draft.leaseEndDate ?? p.leaseEndDate ?? ''}
|
||||
onChange={e => onDraftChange({ ...draft, leaseEndDate: e.target.value })}
|
||||
/>
|
||||
</Box>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.75, mt: 0.5, mb: 0.5 }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.75, mb: 1 }}>
|
||||
<FileText size={13} color="#64748b" />
|
||||
<Typography variant="caption" sx={{ fontWeight: 700, color: '#64748b', textTransform: 'uppercase', letterSpacing: 0.4 }}>
|
||||
Mietvertrag
|
||||
Gebäude-Mietvertrag (Fallback)
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box sx={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 1.5 }}>
|
||||
@@ -122,32 +96,18 @@ export function PropertyDetailOverview({ p, editing, draft, onDraftChange }: Pro
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
) : (
|
||||
<>
|
||||
<FieldGrid>
|
||||
<Field label="Aktueller Mieter" value={p.currentTenant} />
|
||||
<Field label="Mietlaufzeit" value={p.leaseTerm} />
|
||||
<Field label="Mietbeginn" value={p.leaseStartDate ? new Date(p.leaseStartDate).toLocaleDateString('de-CH') : undefined} />
|
||||
<Field label="Mietende" value={p.leaseEndDate ? new Date(p.leaseEndDate).toLocaleDateString('de-CH') : undefined} />
|
||||
<Field label="Breakoutoption" value={p.breakoutOption} />
|
||||
<Field label="Breakoutoption Zeitpunkt" value={p.breakoutOptionDate ? new Date(p.breakoutOptionDate).toLocaleDateString('de-CH') : undefined} />
|
||||
<Field label="Importiert aus" value={p.importedFrom} />
|
||||
<Field label="Zuletzt aktualisiert" value={p.lastUpdatedAt ? new Date(p.lastUpdatedAt).toLocaleDateString('de-CH') : undefined} />
|
||||
</FieldGrid>
|
||||
|
||||
{/* Mietvertrag */}
|
||||
<Box sx={{ mt: 1.5, p: 1.5, border: '1px solid #e2e8f0', borderRadius: 1.5, bgcolor: p.leaseContractUrl ? '#f8fafc' : '#fafafa' }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.75, mb: p.leaseContractUrl ? 0.75 : 0 }}>
|
||||
<FileText size={13} color={p.leaseContractUrl ? '#152642' : '#94a3b8'} />
|
||||
<Typography variant="caption" sx={{ fontWeight: 700, color: p.leaseContractUrl ? '#152642' : '#94a3b8', textTransform: 'uppercase', letterSpacing: 0.4 }}>
|
||||
Mietvertrag
|
||||
</Typography>
|
||||
</Box>
|
||||
{p.leaseContractUrl ? (
|
||||
)
|
||||
}
|
||||
if (!p.leaseContractUrl || hasUnitContracts) return null
|
||||
return (
|
||||
<Box sx={{ mt: 0.5, mb: 1.5, p: 1.5, border: '1px solid #e2e8f0', borderRadius: 1.5, bgcolor: '#f8fafc' }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 1 }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.75 }}>
|
||||
<FileText size={13} color="#152642" />
|
||||
<Typography variant="caption" sx={{ color: '#475569', fontWeight: 500 }} noWrap>
|
||||
{p.leaseContractName ?? 'Mietvertrag'}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Button
|
||||
size="small"
|
||||
variant="outlined"
|
||||
@@ -160,17 +120,9 @@ export function PropertyDetailOverview({ p, editing, draft, onDraftChange }: Pro
|
||||
Öffnen
|
||||
</Button>
|
||||
</Box>
|
||||
) : (
|
||||
<Typography variant="caption" sx={{ color: '#94a3b8' }}>
|
||||
Noch kein Mietvertrag hinterlegt — im Bearbeitungsmodus hinzufügen.
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Floor / unit structure */}
|
||||
<UnitStructurePanel p={p} />
|
||||
)
|
||||
})()}
|
||||
|
||||
{/* Pre-Market Matching */}
|
||||
<PreMarketPanel p={p} />
|
||||
@@ -230,6 +182,8 @@ export function PropertyDetailOverview({ p, editing, draft, onDraftChange }: Pro
|
||||
<Field label="Parkplätze" value={p.hardFacts?.parking ?? p.softFactors?.parkingSpots} />
|
||||
<Field label="ÖV (Min.)" value={p.softFactors?.publicTransportMinutes} />
|
||||
<Field label="Nebenkosten/m²" value={p.ancillaryCosts ? `CHF ${p.ancillaryCosts}` : undefined} />
|
||||
<Field label="Importiert aus" value={p.importedFrom} />
|
||||
<Field label="Zuletzt aktualisiert" value={p.lastUpdatedAt ? new Date(p.lastUpdatedAt).toLocaleDateString('de-CH') : undefined} />
|
||||
</FieldGrid>
|
||||
)}
|
||||
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { useMemo, useState } from 'react'
|
||||
import { Box, Button, Checkbox, Chip, Collapse, Divider, IconButton, Switch, TextField, Tooltip, Typography } from '@mui/material'
|
||||
import { ChevronDown, ChevronUp, Layers, Users } from 'lucide-react'
|
||||
import { ChevronDown, ChevronUp, ExternalLink, FileText, Layers, Users } from 'lucide-react'
|
||||
import { useNavigate } from 'react-router'
|
||||
import type { Property, PropertyUnit, UnitNeedMatch } from '../../domain/property'
|
||||
import type { Lease } from '../../domain/lease'
|
||||
import { useUnitMatchesMap, useUnitBundle, useBundleMatches } from '../../hooks/useUnitMatches'
|
||||
import { useUpdateUnit } from '../../hooks/useProperties'
|
||||
import { DS_BORDER, DS_PRE_MARKET, DS_SURFACE, DS_TEXT } from '../../lib/ds'
|
||||
@@ -24,6 +25,25 @@ function MatchPill({ m }: { m: UnitNeedMatch }) {
|
||||
)
|
||||
}
|
||||
|
||||
function LeaseField({ label, value }: { label: string; value?: string }) {
|
||||
if (!value) return null
|
||||
return (
|
||||
<Box>
|
||||
<Typography sx={{ fontSize: '0.59rem', color: DS_TEXT.muted, textTransform: 'uppercase', letterSpacing: 0.3 }}>
|
||||
{label}
|
||||
</Typography>
|
||||
<Typography sx={{ fontSize: '0.72rem', fontWeight: 600, color: '#1e293b' }}>
|
||||
{value}
|
||||
</Typography>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
function formatLeaseDate(iso?: string): string | undefined {
|
||||
if (!iso) return undefined
|
||||
return new Date(iso).toLocaleDateString('de-CH', { day: 'numeric', month: 'short', year: 'numeric' })
|
||||
}
|
||||
|
||||
export function UnitStructurePanel({ p }: { p: Property }) {
|
||||
const navigate = useNavigate()
|
||||
const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set())
|
||||
@@ -40,6 +60,23 @@ export function UnitStructurePanel({ p }: { p: Property }) {
|
||||
const bundle = useUnitBundle(selectedFreeUnits)
|
||||
const bundleMatches = useBundleMatches(selectedFreeUnits, p)
|
||||
|
||||
// Group units by floor level (consecutive same-floor units form a group)
|
||||
const floorGroups = useMemo(() => {
|
||||
const groups: { floorLevel: number; units: PropertyUnit[] }[] = []
|
||||
for (const u of p.units ?? []) {
|
||||
const last = groups[groups.length - 1]
|
||||
if (last && last.floorLevel === u.floorLevel) {
|
||||
last.units.push(u)
|
||||
} else {
|
||||
groups.push({ floorLevel: u.floorLevel, units: [u] })
|
||||
}
|
||||
}
|
||||
return groups
|
||||
}, [p.units])
|
||||
|
||||
// Flat list of all units for index tracking
|
||||
const allUnits = p.units ?? []
|
||||
|
||||
const toggleUnit = (id: string) => {
|
||||
setSelectedIds(prev => {
|
||||
const next = new Set(prev)
|
||||
@@ -50,34 +87,18 @@ export function UnitStructurePanel({ p }: { p: Property }) {
|
||||
|
||||
if (!p.units || p.units.length === 0) return null
|
||||
|
||||
return (
|
||||
<>
|
||||
<Divider sx={{ my: 2 }} />
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 1 }}>
|
||||
<Typography variant="body2" sx={{ fontWeight: 700, color: '#0f172a', fontSize: '0.8125rem' }}>
|
||||
Stockwerkstruktur
|
||||
</Typography>
|
||||
{freeUnits.length >= 2 && (
|
||||
<Typography variant="caption" sx={{ color: DS_TEXT.muted, fontSize: '0.7rem' }}>
|
||||
Freie Einheiten auswählen zum Kombinieren
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
<Box sx={{ border: `1px solid ${DS_BORDER.default}`, borderRadius: 1.5, overflow: 'hidden', mb: 1.5 }}>
|
||||
{/* Header */}
|
||||
<Box sx={{ display: 'grid', gridTemplateColumns: '28px 72px 88px 1fr auto 80px', bgcolor: DS_SURFACE.neutral.bg, px: 1.5, py: 0.75, borderBottom: `1px solid ${DS_BORDER.default}`, alignItems: 'center' }}>
|
||||
{['', 'Stockwerk', 'Einheit', 'Mieter / Status', 'Matches', 'Fläche'].map(h => (
|
||||
<Typography key={h} variant="caption" sx={{ fontWeight: 600, color: DS_TEXT.muted, fontSize: '0.63rem', textTransform: 'uppercase' }}>{h}</Typography>
|
||||
))}
|
||||
</Box>
|
||||
|
||||
{p.units!.map((u: PropertyUnit, i: number) => {
|
||||
const renderUnitRow = (u: PropertyUnit, isFirstInGroup: boolean, isLastUnit: boolean) => {
|
||||
const matches = u.available ? (unitMatches[u.id] ?? []) : []
|
||||
const topMatch = matches[0]
|
||||
const isExpanded = expandedUnit === u.id
|
||||
const isSelected = selectedIds.has(u.id)
|
||||
const isLastRow = i === p.units!.length - 1 && !isExpanded
|
||||
const unitIndex = allUnits.indexOf(u)
|
||||
const isLastRow = unitIndex === allUnits.length - 1 && !isExpanded
|
||||
|
||||
const activeLease: Lease | undefined = u.leases?.find(l => l.status === 'ACTIVE')
|
||||
const tenantName = activeLease?.tenant.companyName ?? u.currentTenant
|
||||
const leaseEnd = activeLease?.endDate ?? u.leaseEndDate
|
||||
const hasLeaseDetail = !u.available && !!activeLease
|
||||
|
||||
return (
|
||||
<Box key={u.id}>
|
||||
@@ -93,7 +114,7 @@ export function UnitStructurePanel({ p }: { p: Property }) {
|
||||
transition: 'background 0.15s',
|
||||
}}
|
||||
>
|
||||
{/* Checkbox — only for free units */}
|
||||
{/* Checkbox — only for free units when multiple free units exist */}
|
||||
<Box>
|
||||
{u.available && freeUnits.length >= 2 && (
|
||||
<Checkbox
|
||||
@@ -105,13 +126,15 @@ export function UnitStructurePanel({ p }: { p: Property }) {
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{/* Floor */}
|
||||
{/* Floor — only on first unit of group */}
|
||||
<Typography variant="caption" sx={{ fontWeight: 600, color: '#0f172a' }}>
|
||||
{floorLabel(u)}
|
||||
{isFirstInGroup ? floorLabel(u) : ''}
|
||||
</Typography>
|
||||
|
||||
{/* Unit label */}
|
||||
<Typography variant="caption" sx={{ color: '#374151' }}>{u.unitLabel ?? '–'}</Typography>
|
||||
<Typography variant="caption" sx={{ color: '#374151' }}>
|
||||
{u.unitLabel ?? '–'}
|
||||
</Typography>
|
||||
|
||||
{/* Status */}
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.75, flexWrap: 'wrap' }}>
|
||||
@@ -143,7 +166,7 @@ export function UnitStructurePanel({ p }: { p: Property }) {
|
||||
</Box>
|
||||
</Tooltip>
|
||||
|
||||
{/* Inline min-unit editor — show when actively editing OR when teilbar is on but min sqm not set */}
|
||||
{/* Inline min-unit editor */}
|
||||
{(editingFlexUnit === u.id || (u.isFlexible && !u.minLettableSqm)) && (
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
|
||||
<TextField
|
||||
@@ -211,7 +234,7 @@ export function UnitStructurePanel({ p }: { p: Property }) {
|
||||
mieterausbaubeitragPerSqm: p.hardFacts?.mieterausbaubeitragPerSqm,
|
||||
isFlexible: u.isFlexible,
|
||||
minLettableSqm: u.minLettableSqm,
|
||||
availableFrom: u.listing?.availableFrom ?? p.availabilityDate,
|
||||
availableFrom: p.availabilityDate,
|
||||
},
|
||||
},
|
||||
})}
|
||||
@@ -220,14 +243,24 @@ export function UnitStructurePanel({ p }: { p: Property }) {
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
<Typography variant="caption" sx={{ color: DS_TEXT.muted }} noWrap>{u.currentTenant ?? 'Vermietet'}</Typography>
|
||||
<Box>
|
||||
<Typography variant="caption" sx={{ fontWeight: 600, color: '#374151' }} noWrap>
|
||||
{tenantName ?? 'Vermietet'}
|
||||
</Typography>
|
||||
{leaseEnd && (
|
||||
<Typography sx={{ fontSize: '0.6rem', color: DS_TEXT.muted, display: 'block' }}>
|
||||
bis {new Date(leaseEnd).toLocaleDateString('de-CH', { month: 'short', year: '2-digit' })}
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{/* Top match pill + expand */}
|
||||
{/* Matches column + expand */}
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
|
||||
{topMatch && <MatchPill m={topMatch} />}
|
||||
{matches.length > 1 && (
|
||||
{/* Expand for free units with multiple matches */}
|
||||
{u.available && matches.length > 1 && (
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={() => setExpandedUnit(isExpanded ? null : u.id)}
|
||||
@@ -236,6 +269,22 @@ export function UnitStructurePanel({ p }: { p: Property }) {
|
||||
{isExpanded ? <ChevronUp size={12} /> : <ChevronDown size={12} />}
|
||||
</IconButton>
|
||||
)}
|
||||
{/* Expand for occupied units with lease detail */}
|
||||
{hasLeaseDetail && (
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={() => setExpandedUnit(isExpanded ? null : u.id)}
|
||||
sx={{
|
||||
p: 0.5,
|
||||
color: isExpanded ? '#2563eb' : '#64748b',
|
||||
borderRadius: 1,
|
||||
'&:hover': { bgcolor: '#f1f5f9', color: '#2563eb' },
|
||||
transition: 'color 0.15s, background 0.15s',
|
||||
}}
|
||||
>
|
||||
{isExpanded ? <ChevronUp size={14} /> : <ChevronDown size={14} />}
|
||||
</IconButton>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{/* Area */}
|
||||
@@ -251,9 +300,9 @@ export function UnitStructurePanel({ p }: { p: Property }) {
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* Expanded: show all matches for this unit */}
|
||||
<Collapse in={isExpanded}>
|
||||
<Box sx={{ px: 2, py: 1, bgcolor: DS_SURFACE.neutral.bg, borderBottom: i < p.units!.length - 1 ? `1px solid ${DS_BORDER.default}` : 'none' }}>
|
||||
{/* Expanded: matches for free units */}
|
||||
<Collapse in={isExpanded && u.available}>
|
||||
<Box sx={{ px: 2, py: 1, bgcolor: DS_SURFACE.neutral.bg, borderBottom: !isLastUnit ? `1px solid ${DS_BORDER.default}` : 'none' }}>
|
||||
<Typography variant="caption" sx={{ fontWeight: 600, color: DS_TEXT.muted, fontSize: '0.65rem', textTransform: 'uppercase', display: 'block', mb: 0.75 }}>
|
||||
Passende Suchanfragen für diese Einheit
|
||||
</Typography>
|
||||
@@ -273,10 +322,85 @@ export function UnitStructurePanel({ p }: { p: Property }) {
|
||||
</Box>
|
||||
</Box>
|
||||
</Collapse>
|
||||
|
||||
{/* Expanded: lease detail for occupied units */}
|
||||
<Collapse in={isExpanded && !u.available && !!activeLease}>
|
||||
{activeLease && (
|
||||
<Box sx={{ px: 2, py: 1.25, bgcolor: '#f8fafc', borderBottom: !isLastUnit ? `1px solid ${DS_BORDER.default}` : 'none' }}>
|
||||
<Box sx={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: 1.25, mb: activeLease.contractDocumentUrl ? 1.25 : 0 }}>
|
||||
<LeaseField label="Mietbeginn" value={formatLeaseDate(activeLease.startDate)} />
|
||||
<LeaseField label="Mietende" value={formatLeaseDate(activeLease.endDate)} />
|
||||
<LeaseField label="Laufzeit" value={activeLease.contractDurationYears ? `${activeLease.contractDurationYears} Jahre` : undefined} />
|
||||
<LeaseField label="Breakout-Option" value={activeLease.breakoutOption
|
||||
? `Ja${activeLease.breakoutOptionDate ? ` · ${formatLeaseDate(activeLease.breakoutOptionDate)}` : ''}`
|
||||
: 'Nein'
|
||||
} />
|
||||
<LeaseField label="Importiert aus" value={activeLease.sourceSystem} />
|
||||
<LeaseField label="Zuletzt aktualisiert" value={formatLeaseDate(activeLease.lastUpdatedAt)} />
|
||||
</Box>
|
||||
{activeLease.contractDocumentUrl && (
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.75, pt: 0.75, borderTop: `1px solid ${DS_BORDER.muted}` }}>
|
||||
<FileText size={11} color="#152642" />
|
||||
<Typography variant="caption" sx={{ fontWeight: 500, color: '#475569', flex: 1 }} noWrap>
|
||||
{activeLease.contractDocumentName ?? 'Mietvertrag'}
|
||||
</Typography>
|
||||
<Button
|
||||
size="small"
|
||||
variant="outlined"
|
||||
endIcon={<ExternalLink size={10} />}
|
||||
href={activeLease.contractDocumentUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
sx={{ textTransform: 'none', fontSize: '0.68rem', py: 0.2, px: 0.875, borderColor: '#cbd5e1', color: '#152642', flexShrink: 0 }}
|
||||
>
|
||||
Öffnen
|
||||
</Button>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
</Collapse>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Divider sx={{ my: 2 }} />
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 1 }}>
|
||||
<Typography variant="body2" sx={{ fontWeight: 700, color: '#0f172a', fontSize: '0.8125rem' }}>
|
||||
Einheiten & Mietverträge
|
||||
</Typography>
|
||||
{freeUnits.length >= 2 && (
|
||||
<Typography variant="caption" sx={{ color: DS_TEXT.muted, fontSize: '0.7rem' }}>
|
||||
Freie Einheiten auswählen zum Kombinieren
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
<Box sx={{ border: `1px solid ${DS_BORDER.default}`, borderRadius: 1.5, overflow: 'hidden', mb: 1.5 }}>
|
||||
{/* Header */}
|
||||
<Box sx={{ display: 'grid', gridTemplateColumns: '28px 72px 88px 1fr auto 80px', bgcolor: DS_SURFACE.neutral.bg, px: 1.5, py: 0.75, borderBottom: `1px solid ${DS_BORDER.default}`, alignItems: 'center' }}>
|
||||
{['', 'Stockwerk', 'Einheit', 'Mieter / Status', '', 'Fläche'].map((h, i) => (
|
||||
<Typography key={i} variant="caption" sx={{ fontWeight: 600, color: DS_TEXT.muted, fontSize: '0.63rem', textTransform: 'uppercase' }}>{h}</Typography>
|
||||
))}
|
||||
</Box>
|
||||
|
||||
{/* Floor groups */}
|
||||
{floorGroups.map((group, groupIdx) => (
|
||||
<Box
|
||||
key={group.floorLevel}
|
||||
sx={{ borderTop: groupIdx > 0 ? `2px solid ${DS_BORDER.default}` : 'none' }}
|
||||
>
|
||||
{group.units.map((u, unitIdxInGroup) => {
|
||||
const isFirstInGroup = unitIdxInGroup === 0
|
||||
const globalIdx = allUnits.indexOf(u)
|
||||
const isLastUnit = globalIdx === allUnits.length - 1
|
||||
return renderUnitRow(u, isFirstInGroup, isLastUnit)
|
||||
})}
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
|
||||
{/* Bundle panel */}
|
||||
{bundle && (
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
export * from './enums'
|
||||
export * from './tenant'
|
||||
export * from './lease'
|
||||
export * from './property'
|
||||
export * from './need'
|
||||
export * from './match'
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
import type { Tenant } from './tenant'
|
||||
|
||||
export interface Lease {
|
||||
id: string
|
||||
unitId: string
|
||||
tenant: Tenant
|
||||
rentPerSqm: number // CHF/m²/Jahr
|
||||
startDate: string // ISO date
|
||||
endDate: string // ISO date
|
||||
contractDurationYears?: number
|
||||
breakoutOption?: boolean
|
||||
breakoutOptionDate?: string
|
||||
status: 'ACTIVE' | 'FUTURE' | 'EXPIRED'
|
||||
sourceSystem?: 'SAP_REFX' | 'GARAIO_REM' | 'MANUAL' | string
|
||||
sourceId?: string // ID in source ERP
|
||||
importedAt?: string
|
||||
lastUpdatedAt?: string
|
||||
contractDocumentUrl?: string
|
||||
contractDocumentName?: string
|
||||
}
|
||||
+32
-5
@@ -4,6 +4,7 @@ import type {
|
||||
} from './enums'
|
||||
import { AvailabilityStatus as AS } from './enums'
|
||||
import type { PropertyUnit } from './unit'
|
||||
import type { Lease } from './lease'
|
||||
|
||||
// Re-export unit types so all existing imports from 'property' continue to work
|
||||
export type { PropertyUnit, UnitBundle, UnitNeedMatch } from './unit'
|
||||
@@ -137,18 +138,26 @@ export interface Property {
|
||||
description?: string
|
||||
images?: string[]
|
||||
floorPlanUrl?: string
|
||||
leaseContractUrl?: string // link to the signed lease document
|
||||
leaseContractName?: string // display label, e.g. "Mietvertrag 2021–2026"
|
||||
/** @deprecated Use unit.leases[].contractDocumentUrl instead */
|
||||
leaseContractUrl?: string
|
||||
/** @deprecated Use unit.leases[].contractDocumentName instead */
|
||||
leaseContractName?: string
|
||||
|
||||
propertyNumber?: string
|
||||
units?: PropertyUnit[]
|
||||
mapImageUrl?: string
|
||||
|
||||
/** @deprecated Use unit.leases instead */
|
||||
leaseTerm?: string
|
||||
/** @deprecated Use unit.leases[].startDate instead */
|
||||
leaseStartDate?: string
|
||||
/** @deprecated Use unit.leases[].endDate instead */
|
||||
leaseEndDate?: string
|
||||
/** @deprecated Use unit.leases[].breakoutOption instead */
|
||||
breakoutOption?: boolean
|
||||
/** @deprecated Use unit.leases[].breakoutOptionDate instead */
|
||||
breakoutOptionDate?: string
|
||||
/** @deprecated Use unit.leases[].tenant.companyName instead */
|
||||
currentTenant?: string
|
||||
importedFrom?: string
|
||||
importedAt?: string
|
||||
@@ -169,9 +178,29 @@ export type UpdatePropertyInput = Partial<CreatePropertyInput>
|
||||
* Returns the rentable units of a property.
|
||||
* For properties without explicit units, synthesises one unit from property-level data
|
||||
* so that all matching logic can operate uniformly at unit level.
|
||||
* Property-level lease fields are synthesised into a proper Lease object for backward compat.
|
||||
*/
|
||||
export function getEffectiveUnits(p: Property): PropertyUnit[] {
|
||||
if (p.units && p.units.length > 0) return p.units
|
||||
|
||||
const syntheticLease: Lease | undefined = p.currentTenant
|
||||
? {
|
||||
id: `${p.id}-lease`,
|
||||
unitId: p.id,
|
||||
tenant: { id: `${p.id}-tenant`, companyName: p.currentTenant },
|
||||
rentPerSqm: p.rentPricePerSqm,
|
||||
startDate: p.leaseStartDate ?? '',
|
||||
endDate: p.leaseEndDate ?? '',
|
||||
contractDurationYears: p.contractDurationMonths ? Math.round(p.contractDurationMonths / 12) : undefined,
|
||||
breakoutOption: p.breakoutOption,
|
||||
breakoutOptionDate: p.breakoutOptionDate,
|
||||
status: p.leaseEndDate && new Date(p.leaseEndDate) > new Date() ? 'ACTIVE' : 'EXPIRED',
|
||||
sourceSystem: p.importedFrom ?? 'MANUAL',
|
||||
contractDocumentUrl: p.leaseContractUrl,
|
||||
contractDocumentName: p.leaseContractName,
|
||||
}
|
||||
: undefined
|
||||
|
||||
return [{
|
||||
id: p.id,
|
||||
propertyId: p.id,
|
||||
@@ -180,9 +209,7 @@ export function getEffectiveUnits(p: Property): PropertyUnit[] {
|
||||
areaSqm: p.areaSqm,
|
||||
available: p.availabilityStatus === AS.AVAILABLE_NOW || p.availabilityStatus === AS.AVAILABLE_SOON,
|
||||
rentPricePerSqm: p.rentPricePerSqm,
|
||||
currentTenant: p.currentTenant,
|
||||
leaseTerm: p.leaseTerm,
|
||||
leaseEndDate: p.leaseEndDate,
|
||||
leases: syntheticLease ? [syntheticLease] : [],
|
||||
schattenmarktRelease: p.schattenmarktRelease?.enabled
|
||||
? { enabled: true, availableFrom: p.leaseEndDate }
|
||||
: undefined,
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
export interface Tenant {
|
||||
id: string
|
||||
companyName: string
|
||||
industry?: string
|
||||
contactPerson?: {
|
||||
name: string
|
||||
email?: string
|
||||
phone?: string
|
||||
}
|
||||
}
|
||||
+32
-7
@@ -1,22 +1,30 @@
|
||||
// ── Rentable Unit — primary entity for demand-side matching ──────────────────
|
||||
//
|
||||
// A Property is the building/address container.
|
||||
// A PropertyUnit is what is actually rented: a specific floor, wing, or section.
|
||||
// Floor level is business-critical: retail needs EG for walk-in traffic,
|
||||
// offices can occupy upper floors, logistics needs ground-level loading access.
|
||||
// A Property is the building/address container (Liegenschaft).
|
||||
// A PropertyUnit is what is actually rented: a specific floor, wing, or section (Mietobjekt).
|
||||
// Floor level is an attribute of the unit — not a separate entity (matches Garaio REM / SAP RE-FX).
|
||||
// Tenant and contract data live on Lease, not directly on the unit.
|
||||
|
||||
import type { Lease } from './lease'
|
||||
import type { AvailabilityStatus } from './enums'
|
||||
import { AvailabilityStatus as AS } from './enums'
|
||||
|
||||
export interface PropertyUnit {
|
||||
id: string
|
||||
propertyId?: string // FK to parent Property
|
||||
propertyId?: string // FK to parent Property (Liegenschaft)
|
||||
floorLevel: number // 0=EG, 1=1.OG, -1=UG1
|
||||
unitLabel?: string // e.g. "Nord", "West", "Einheit A"
|
||||
areaSqm: number
|
||||
available: boolean
|
||||
rentPricePerSqm?: number // annual CHF/m²; falls back to property.rentPricePerSqm
|
||||
leases?: Lease[] // Mietverträge — current, historical, future
|
||||
/** @deprecated Use leases[].tenant.companyName */
|
||||
currentTenant?: string
|
||||
leaseTerm?: string
|
||||
/** @deprecated Use leases[].endDate */
|
||||
leaseEndDate?: string
|
||||
floorPlanUrl?: string // optional floor plan image
|
||||
/** @deprecated Use leases[0].contractDurationYears */
|
||||
leaseTerm?: string
|
||||
floorPlanUrl?: string
|
||||
// Flexible letting (Teilfläche)
|
||||
isFlexible?: boolean
|
||||
minLettableSqm?: number
|
||||
@@ -28,6 +36,23 @@ export interface PropertyUnit {
|
||||
}
|
||||
}
|
||||
|
||||
// Derives availability from the unit's lease list.
|
||||
// Use this instead of reading property.availabilityStatus directly.
|
||||
export function getUnitAvailability(unit: PropertyUnit): {
|
||||
status: AvailabilityStatus
|
||||
availableFrom?: string
|
||||
activeLease?: Lease
|
||||
} {
|
||||
const active = unit.leases?.find(l => l.status === 'ACTIVE')
|
||||
if (!active) return { status: AS.AVAILABLE_NOW }
|
||||
const now = new Date()
|
||||
const monthsToEnd = (new Date(active.endDate).getTime() - now.getTime()) / (1000 * 60 * 60 * 24 * 30)
|
||||
if (monthsToEnd <= 12) {
|
||||
return { status: AS.AVAILABLE_SOON, availableFrom: active.endDate, activeLease: active }
|
||||
}
|
||||
return { status: AS.OCCUPIED, activeLease: active }
|
||||
}
|
||||
|
||||
// A bundle groups multiple free units into a combined offer
|
||||
export interface UnitBundle {
|
||||
unitIds: string[]
|
||||
|
||||
@@ -11,8 +11,9 @@ import type {
|
||||
MatchCardReason,
|
||||
} from '../../components/match-card/MatchCardViewModel'
|
||||
import { getCityIntelligence } from '../../lib/locationIntelligence'
|
||||
import { formatUnitTitle, formatMultiUnitFloors } from '../../domain/unit'
|
||||
import { formatUnitTitle, formatMultiUnitFloors, getUnitAvailability } from '../../domain/unit'
|
||||
import { calcFitOutInvestment } from '../../lib/fitOutUtils'
|
||||
import { AvailabilityStatus } from '../../domain/enums'
|
||||
|
||||
const HARD_CRITERIA = new Set(['area', 'location', 'budget', 'timing'])
|
||||
|
||||
@@ -69,9 +70,19 @@ export function buildMatchCardViewModel(
|
||||
cardTitle = property?.title ?? signal?.companyName ?? signal?.locationHint ?? '–'
|
||||
}
|
||||
|
||||
const availabilityLabel =
|
||||
property?.availabilityDate ??
|
||||
(signal?.timeHorizonMonths ? `~${signal.timeHorizonMonths} Monate` : undefined)
|
||||
// Prefer unit-level availability (from Lease), fall back to property-level date, then signal
|
||||
const unitAvail = unit ? getUnitAvailability(unit) : null
|
||||
const availabilityLabel = (() => {
|
||||
if (unitAvail?.availableFrom) {
|
||||
return `ab ${new Date(unitAvail.availableFrom).toLocaleDateString('de-CH', { month: 'short', year: '2-digit' })}`
|
||||
}
|
||||
if (unitAvail?.status === AvailabilityStatus.AVAILABLE_NOW) return 'Sofort verfügbar'
|
||||
if (property?.availabilityDate) {
|
||||
return `ab ${new Date(property.availabilityDate).toLocaleDateString('de-CH', { month: 'short', year: '2-digit' })}`
|
||||
}
|
||||
if (signal?.timeHorizonMonths) return `~${signal.timeHorizonMonths} Monate`
|
||||
return undefined
|
||||
})()
|
||||
|
||||
const sourceLabel =
|
||||
property?.sourceLabel ??
|
||||
@@ -134,6 +145,8 @@ export function buildMatchCardViewModel(
|
||||
signalConfirmedFacts: signal?.confirmedFacts,
|
||||
signalUnconfirmedFacts: signal?.unconfirmedFacts,
|
||||
signalAreaSqmEstimate: signal?.areaSqmEstimate,
|
||||
areaSqm: unit?.areaSqm ?? property?.areaSqm ?? signal?.areaSqmEstimate,
|
||||
rentPerSqm: property?.rentPricePerSqm,
|
||||
propertyId: property?.id ?? signal?.propertyId,
|
||||
unitId: match.unitId ?? unit?.id ?? signal?.unitId,
|
||||
preMarketUnit: unit,
|
||||
|
||||
+18
-18
@@ -49,9 +49,9 @@ export const mockProperties: Property[] = [
|
||||
description: 'Moderne Bürofläche im aufstrebenden Stadtquartier Zürich-West, direkt beim Trendviertel Freilager. Die hellen, offen gestalteten Flächen bieten optimale Bedingungen für kollaboratives Arbeiten. Grosszügige Fensterfronten sorgen für viel Tageslicht. Das Gebäude verfügt über einen repräsentativen Empfangsbereich, Sitzungsräume sowie eine Gemeinschaftsterrasse mit Blick auf die Stadt. ÖV-Anbindung in unmittelbarer Nähe (Tram 4/13, S-Bahn Hardbrücke, 4 Minuten zu Fuss).',
|
||||
|
||||
units: [
|
||||
{ id: 'unit-001-1', propertyId: 'prop-001', floorLevel: 1, unitLabel: 'Nord', areaSqm: 280, available: false, rentPricePerSqm: 456, currentTenant: 'MediaGroup Schweiz AG', leaseTerm: '5 Jahre', leaseEndDate: '2026-10-31', schattenmarktRelease: { enabled: true, availableFrom: '2026-10-31' } },
|
||||
{ id: 'unit-001-2', propertyId: 'prop-001', floorLevel: 2, unitLabel: 'Süd', areaSqm: 310, available: false, rentPricePerSqm: 456, currentTenant: 'MediaGroup Schweiz AG', leaseTerm: '5 Jahre', leaseEndDate: '2026-10-31', schattenmarktRelease: { enabled: true, availableFrom: '2026-10-31' } },
|
||||
{ id: 'unit-001-3', propertyId: 'prop-001', floorLevel: 3, unitLabel: 'West', areaSqm: 260, available: true, rentPricePerSqm: 480, isFlexible: true, minLettableSqm: 120 },
|
||||
{ id: 'unit-001-1', propertyId: 'prop-001', floorLevel: 1, unitLabel: 'Nord', areaSqm: 280, available: false, rentPricePerSqm: 456, leases: [{ id: 'lease-001-1', unitId: 'unit-001-1', tenant: { id: 'ten-media-001', companyName: 'MediaGroup Schweiz AG', industry: 'Media & Kommunikation' }, rentPerSqm: 456, startDate: '2020-09-01', endDate: '2026-10-31', contractDurationYears: 5, breakoutOption: false, status: 'ACTIVE', sourceSystem: 'SAP_REFX', sourceId: 'MV-2020-001', importedAt: '2025-01-15T08:00:00Z', lastUpdatedAt: '2025-05-10T10:00:00Z', contractDocumentUrl: '/docs/mietvertrag-media-2020.pdf', contractDocumentName: 'Mietvertrag 2020–2026' }], schattenmarktRelease: { enabled: true, availableFrom: '2026-10-31' } },
|
||||
{ id: 'unit-001-2', propertyId: 'prop-001', floorLevel: 2, unitLabel: 'Süd', areaSqm: 310, available: false, rentPricePerSqm: 456, leases: [{ id: 'lease-001-2', unitId: 'unit-001-2', tenant: { id: 'ten-media-001', companyName: 'MediaGroup Schweiz AG', industry: 'Media & Kommunikation' }, rentPerSqm: 456, startDate: '2020-09-01', endDate: '2026-10-31', contractDurationYears: 5, breakoutOption: false, status: 'ACTIVE', sourceSystem: 'SAP_REFX', sourceId: 'MV-2020-002', importedAt: '2025-01-15T08:00:00Z', lastUpdatedAt: '2025-05-10T10:00:00Z', contractDocumentUrl: '/docs/mietvertrag-media-2020.pdf', contractDocumentName: 'Mietvertrag 2020–2026' }], schattenmarktRelease: { enabled: true, availableFrom: '2026-10-31' } },
|
||||
{ id: 'unit-001-3', propertyId: 'prop-001', floorLevel: 3, unitLabel: 'West', areaSqm: 260, available: true, rentPricePerSqm: 480, leases: [], isFlexible: true, minLettableSqm: 120 },
|
||||
],
|
||||
importedFrom: 'SAP RE-FX',
|
||||
importedAt: '2025-01-15T08:00:00Z',
|
||||
@@ -106,8 +106,8 @@ export const mockProperties: Property[] = [
|
||||
propertyNumber: 'BS-2022-002',
|
||||
description: 'Grossflächige Logistikanlage in direkter Rheinnähe, Kleinhüningen Basel. Zwei separate Lagerhallen A und B mit je eigenem Tor und Rampenanlage. Sprinkleranlage und Hallentemperierung vorhanden. Sehr gute Erschliessung via A2/A3, 30 Lastwagenstellplätze auf dem Areal. Ausbaupotenzial von 800 m² auf dem Grundstück verfügbar.',
|
||||
units: [
|
||||
{ id: 'unit-002-1', propertyId: 'prop-002', floorLevel: 0, unitLabel: 'Halle A', areaSqm: 1400, available: false, rentPricePerSqm: 168, currentTenant: 'Spedition Rhein GmbH', leaseTerm: '3 Jahre', leaseEndDate: '2026-09-30', schattenmarktRelease: { enabled: true, availableFrom: '2026-09-30' } },
|
||||
{ id: 'unit-002-2', propertyId: 'prop-002', floorLevel: 0, unitLabel: 'Halle B', areaSqm: 1000, available: false, rentPricePerSqm: 168, currentTenant: 'Spedition Rhein GmbH', leaseTerm: '3 Jahre', leaseEndDate: '2026-09-30', schattenmarktRelease: { enabled: true, availableFrom: '2026-09-30' } },
|
||||
{ id: 'unit-002-1', propertyId: 'prop-002', floorLevel: 0, unitLabel: 'Halle A', areaSqm: 1400, available: false, rentPricePerSqm: 168, leases: [{ id: 'lease-002-1', unitId: 'unit-002-1', tenant: { id: 'ten-spedition-002', companyName: 'Spedition Rhein GmbH', industry: 'Logistik & Transport' }, rentPerSqm: 168, startDate: '2022-07-01', endDate: '2026-09-30', contractDurationYears: 3, breakoutOption: false, status: 'ACTIVE', sourceSystem: 'SAP_REFX', sourceId: 'MV-2022-001', importedAt: '2025-01-15T08:00:00Z', lastUpdatedAt: '2025-05-10T10:00:00Z' }], schattenmarktRelease: { enabled: true, availableFrom: '2026-09-30' } },
|
||||
{ id: 'unit-002-2', propertyId: 'prop-002', floorLevel: 0, unitLabel: 'Halle B', areaSqm: 1000, available: false, rentPricePerSqm: 168, leases: [{ id: 'lease-002-2', unitId: 'unit-002-2', tenant: { id: 'ten-spedition-002', companyName: 'Spedition Rhein GmbH', industry: 'Logistik & Transport' }, rentPerSqm: 168, startDate: '2022-07-01', endDate: '2026-09-30', contractDurationYears: 3, breakoutOption: false, status: 'ACTIVE', sourceSystem: 'SAP_REFX', sourceId: 'MV-2022-002', importedAt: '2025-01-15T08:00:00Z', lastUpdatedAt: '2025-05-10T10:00:00Z' }], schattenmarktRelease: { enabled: true, availableFrom: '2026-09-30' } },
|
||||
],
|
||||
importedFrom: 'SAP RE-FX',
|
||||
importedAt: '2025-01-15T08:00:00Z',
|
||||
@@ -164,9 +164,9 @@ export const mockProperties: Property[] = [
|
||||
propertyNumber: 'ZH-2021-007',
|
||||
description: 'Helle Büroflächen im 2. Obergeschoss an der Thurgauerstrasse, Zürich-Oerlikon. Drei Einheiten — zwei belegt und für Pre-Market freigegeben, eine flexible Einheit ab 150 m² direkt verfügbar. Hervorragende ÖV-Anbindung via Tram 11 und S-Bahn Oerlikon. Break-out-Option auf September 2026, danach gesamte 720 m² frei.',
|
||||
units: [
|
||||
{ id: 'unit-007-1', propertyId: 'prop-007', floorLevel: 0, unitLabel: 'EG Ost', areaSqm: 180, available: false, rentPricePerSqm: 420, currentTenant: 'Consulting Partners AG', leaseTerm: '4 Jahre', leaseEndDate: '2026-11-30', schattenmarktRelease: { enabled: true, availableFrom: '2026-11-30' } },
|
||||
{ id: 'unit-007-2', propertyId: 'prop-007', floorLevel: 1, unitLabel: '1.OG', areaSqm: 260, available: false, rentPricePerSqm: 432, currentTenant: 'Consulting Partners AG', leaseTerm: '4 Jahre', leaseEndDate: '2026-11-30', schattenmarktRelease: { enabled: true, availableFrom: '2026-11-30' } },
|
||||
{ id: 'unit-007-3', propertyId: 'prop-007', floorLevel: 2, unitLabel: '2.OG West', areaSqm: 280, available: true, rentPricePerSqm: 445, isFlexible: true, minLettableSqm: 150 },
|
||||
{ id: 'unit-007-1', propertyId: 'prop-007', floorLevel: 0, unitLabel: 'EG Ost', areaSqm: 180, available: false, rentPricePerSqm: 420, leases: [{ id: 'lease-007-1', unitId: 'unit-007-1', tenant: { id: 'ten-consulting-007', companyName: 'Consulting Partners AG', industry: 'Unternehmensberatung' }, rentPerSqm: 420, startDate: '2021-10-01', endDate: '2026-11-30', contractDurationYears: 4, breakoutOption: true, breakoutOptionDate: '2026-09-01', status: 'ACTIVE', sourceSystem: 'SAP_REFX', importedAt: '2025-01-15T08:00:00Z', lastUpdatedAt: '2025-05-10T10:00:00Z' }], schattenmarktRelease: { enabled: true, availableFrom: '2026-11-30' } },
|
||||
{ id: 'unit-007-2', propertyId: 'prop-007', floorLevel: 1, unitLabel: '1.OG', areaSqm: 260, available: false, rentPricePerSqm: 432, leases: [{ id: 'lease-007-2', unitId: 'unit-007-2', tenant: { id: 'ten-consulting-007', companyName: 'Consulting Partners AG', industry: 'Unternehmensberatung' }, rentPerSqm: 432, startDate: '2021-10-01', endDate: '2026-11-30', contractDurationYears: 4, breakoutOption: true, breakoutOptionDate: '2026-09-01', status: 'ACTIVE', sourceSystem: 'SAP_REFX', importedAt: '2025-01-15T08:00:00Z', lastUpdatedAt: '2025-05-10T10:00:00Z' }], schattenmarktRelease: { enabled: true, availableFrom: '2026-11-30' } },
|
||||
{ id: 'unit-007-3', propertyId: 'prop-007', floorLevel: 2, unitLabel: '2.OG West', areaSqm: 280, available: true, rentPricePerSqm: 445, leases: [], isFlexible: true, minLettableSqm: 150 },
|
||||
],
|
||||
importedFrom: 'SAP RE-FX',
|
||||
importedAt: '2025-01-15T08:00:00Z',
|
||||
@@ -223,10 +223,10 @@ export const mockProperties: Property[] = [
|
||||
propertyNumber: 'BS-2021-008',
|
||||
description: 'Moderne Büroflächen im renommierten Dreispitz-Areal, Hochbergerstrasse Basel. Vier unabhängige Etagen für verschiedene Teams oder Mieter. Parkhaus im Gebäude vorhanden, hervorragende Anbindung an Tram 11. Derzeit vollvermietet an Pharma Research GmbH — alle Einheiten für Pre-Market freigegeben.',
|
||||
units: [
|
||||
{ id: 'unit-008-1', propertyId: 'prop-008', floorLevel: 1, unitLabel: '1.OG', areaSqm: 220, available: false, rentPricePerSqm: 384, currentTenant: 'Pharma Research GmbH', leaseTerm: '6 Jahre', leaseEndDate: '2027-08-31', schattenmarktRelease: { enabled: true, availableFrom: '2027-08-31' } },
|
||||
{ id: 'unit-008-2', propertyId: 'prop-008', floorLevel: 2, unitLabel: '2.OG West', areaSqm: 250, available: false, rentPricePerSqm: 384, currentTenant: 'Pharma Research GmbH', leaseTerm: '6 Jahre', leaseEndDate: '2027-08-31', schattenmarktRelease: { enabled: true, availableFrom: '2027-08-31' } },
|
||||
{ id: 'unit-008-3', propertyId: 'prop-008', floorLevel: 3, unitLabel: '3.OG Ost', areaSqm: 230, available: false, rentPricePerSqm: 384, currentTenant: 'Pharma Research GmbH', leaseTerm: '6 Jahre', leaseEndDate: '2027-08-31', schattenmarktRelease: { enabled: true, availableFrom: '2027-08-31' } },
|
||||
{ id: 'unit-008-4', propertyId: 'prop-008', floorLevel: 4, unitLabel: '4.OG', areaSqm: 200, available: false, rentPricePerSqm: 408, currentTenant: 'Pharma Research GmbH', leaseTerm: '6 Jahre', leaseEndDate: '2027-08-31', schattenmarktRelease: { enabled: false } },
|
||||
{ id: 'unit-008-1', propertyId: 'prop-008', floorLevel: 1, unitLabel: '1.OG', areaSqm: 220, available: false, rentPricePerSqm: 384, leases: [{ id: 'lease-008-1', unitId: 'unit-008-1', tenant: { id: 'ten-pharma-008', companyName: 'Pharma Research GmbH', industry: 'Pharma & Life Sciences' }, rentPerSqm: 384, startDate: '2021-09-01', endDate: '2027-08-31', contractDurationYears: 6, breakoutOption: false, status: 'ACTIVE', sourceSystem: 'SAP_REFX', importedAt: '2025-01-15T08:00:00Z', lastUpdatedAt: '2025-05-10T10:00:00Z' }], schattenmarktRelease: { enabled: true, availableFrom: '2027-08-31' } },
|
||||
{ id: 'unit-008-2', propertyId: 'prop-008', floorLevel: 2, unitLabel: '2.OG West', areaSqm: 250, available: false, rentPricePerSqm: 384, leases: [{ id: 'lease-008-2', unitId: 'unit-008-2', tenant: { id: 'ten-pharma-008', companyName: 'Pharma Research GmbH', industry: 'Pharma & Life Sciences' }, rentPerSqm: 384, startDate: '2021-09-01', endDate: '2027-08-31', contractDurationYears: 6, breakoutOption: false, status: 'ACTIVE', sourceSystem: 'SAP_REFX', importedAt: '2025-01-15T08:00:00Z', lastUpdatedAt: '2025-05-10T10:00:00Z' }], schattenmarktRelease: { enabled: true, availableFrom: '2027-08-31' } },
|
||||
{ id: 'unit-008-3', propertyId: 'prop-008', floorLevel: 3, unitLabel: '3.OG Ost', areaSqm: 230, available: false, rentPricePerSqm: 384, leases: [{ id: 'lease-008-3', unitId: 'unit-008-3', tenant: { id: 'ten-pharma-008', companyName: 'Pharma Research GmbH', industry: 'Pharma & Life Sciences' }, rentPerSqm: 384, startDate: '2021-09-01', endDate: '2027-08-31', contractDurationYears: 6, breakoutOption: false, status: 'ACTIVE', sourceSystem: 'SAP_REFX', importedAt: '2025-01-15T08:00:00Z', lastUpdatedAt: '2025-05-10T10:00:00Z' }], schattenmarktRelease: { enabled: true, availableFrom: '2027-08-31' } },
|
||||
{ id: 'unit-008-4', propertyId: 'prop-008', floorLevel: 4, unitLabel: '4.OG', areaSqm: 200, available: false, rentPricePerSqm: 408, leases: [{ id: 'lease-008-4', unitId: 'unit-008-4', tenant: { id: 'ten-pharma-008', companyName: 'Pharma Research GmbH', industry: 'Pharma & Life Sciences' }, rentPerSqm: 408, startDate: '2021-09-01', endDate: '2027-08-31', contractDurationYears: 6, breakoutOption: false, status: 'ACTIVE', sourceSystem: 'SAP_REFX', importedAt: '2025-01-15T08:00:00Z', lastUpdatedAt: '2025-05-10T10:00:00Z' }], schattenmarktRelease: { enabled: false } },
|
||||
],
|
||||
importedFrom: 'SAP RE-FX',
|
||||
importedAt: '2025-01-15T08:00:00Z',
|
||||
@@ -281,8 +281,8 @@ export const mockProperties: Property[] = [
|
||||
propertyNumber: 'ZH-2022-009',
|
||||
description: 'Modernes Logistikzentrum im Töss-Quartier Winterthur, mit direkter A1-Anbindung. Lager Nord und Süd können separat oder gemeinsam angemietet werden. Ebene Andienung mit 4 Toren, Hallenhöhe 8 m, Bodenbelastung 3 t/m². 25 Lastwagenstellplätze. Ausbaupotenzial von 600 m² vorhanden.',
|
||||
units: [
|
||||
{ id: 'unit-009-1', propertyId: 'prop-009', floorLevel: 0, unitLabel: 'Lager Nord', areaSqm: 900, available: false, rentPricePerSqm: 156, currentTenant: 'Sperrgut Logistik AG', leaseTerm: '6 Jahre', leaseEndDate: '2026-10-31', schattenmarktRelease: { enabled: true, availableFrom: '2026-10-31' } },
|
||||
{ id: 'unit-009-2', propertyId: 'prop-009', floorLevel: 0, unitLabel: 'Lager Süd', areaSqm: 900, available: false, rentPricePerSqm: 156, currentTenant: 'Sperrgut Logistik AG', leaseTerm: '6 Jahre', leaseEndDate: '2026-10-31', schattenmarktRelease: { enabled: true, availableFrom: '2026-10-31' } },
|
||||
{ id: 'unit-009-1', propertyId: 'prop-009', floorLevel: 0, unitLabel: 'Lager Nord', areaSqm: 900, available: false, rentPricePerSqm: 156, leases: [{ id: 'lease-009-1', unitId: 'unit-009-1', tenant: { id: 'ten-sperrgut-009', companyName: 'Sperrgut Logistik AG', industry: 'Logistik & Transport' }, rentPerSqm: 156, startDate: '2020-10-01', endDate: '2026-10-31', contractDurationYears: 6, breakoutOption: false, status: 'ACTIVE', sourceSystem: 'SAP_REFX', importedAt: '2025-01-15T08:00:00Z', lastUpdatedAt: '2025-05-10T10:00:00Z' }], schattenmarktRelease: { enabled: true, availableFrom: '2026-10-31' } },
|
||||
{ id: 'unit-009-2', propertyId: 'prop-009', floorLevel: 0, unitLabel: 'Lager Süd', areaSqm: 900, available: false, rentPricePerSqm: 156, leases: [{ id: 'lease-009-2', unitId: 'unit-009-2', tenant: { id: 'ten-sperrgut-009', companyName: 'Sperrgut Logistik AG', industry: 'Logistik & Transport' }, rentPerSqm: 156, startDate: '2020-10-01', endDate: '2026-10-31', contractDurationYears: 6, breakoutOption: false, status: 'ACTIVE', sourceSystem: 'SAP_REFX', importedAt: '2025-01-15T08:00:00Z', lastUpdatedAt: '2025-05-10T10:00:00Z' }], schattenmarktRelease: { enabled: true, availableFrom: '2026-10-31' } },
|
||||
],
|
||||
importedFrom: 'SAP RE-FX',
|
||||
importedAt: '2025-01-15T08:00:00Z',
|
||||
@@ -337,8 +337,8 @@ export const mockProperties: Property[] = [
|
||||
propertyNumber: 'ZH-2024-010',
|
||||
description: 'Exklusive Retailfläche am Löwenplatz, Zürich-Innenstadt. Direkter Zugang zu Bahnhof und Tramknotenpunkt, maximale Laufkundschaft rund um die Uhr. EG-Verkaufsfläche mit repräsentativer Schaufensterfront. Derzeit von Fashion Concept GmbH belegt — Pre-Market-Freigabe bereits aktiv.',
|
||||
units: [
|
||||
{ id: 'unit-010-1', propertyId: 'prop-010', floorLevel: 0, unitLabel: 'EG Verkauf', areaSqm: 205, available: false, rentPricePerSqm: 1056, currentTenant: 'Fashion Concept GmbH', leaseTerm: '6 Jahre', leaseEndDate: '2026-09-30', schattenmarktRelease: { enabled: true, availableFrom: '2026-09-30' } },
|
||||
{ id: 'unit-010-2', propertyId: 'prop-010', floorLevel: -1, unitLabel: 'UG Lager', areaSqm: 80, available: false, rentPricePerSqm: 432, currentTenant: 'Fashion Concept GmbH', leaseTerm: '6 Jahre', leaseEndDate: '2026-09-30', schattenmarktRelease: { enabled: false } },
|
||||
{ id: 'unit-010-1', propertyId: 'prop-010', floorLevel: 0, unitLabel: 'EG Verkauf', areaSqm: 205, available: false, rentPricePerSqm: 1056, leases: [{ id: 'lease-010-1', unitId: 'unit-010-1', tenant: { id: 'ten-fashion-010', companyName: 'Fashion Concept GmbH', industry: 'Retail & Mode' }, rentPerSqm: 1056, startDate: '2020-09-01', endDate: '2026-09-30', contractDurationYears: 6, breakoutOption: false, status: 'ACTIVE', sourceSystem: 'SAP_REFX', importedAt: '2025-01-15T08:00:00Z', lastUpdatedAt: '2025-05-10T10:00:00Z' }], schattenmarktRelease: { enabled: true, availableFrom: '2026-09-30' } },
|
||||
{ id: 'unit-010-2', propertyId: 'prop-010', floorLevel: -1, unitLabel: 'UG Lager', areaSqm: 80, available: false, rentPricePerSqm: 432, leases: [{ id: 'lease-010-2', unitId: 'unit-010-2', tenant: { id: 'ten-fashion-010', companyName: 'Fashion Concept GmbH', industry: 'Retail & Mode' }, rentPerSqm: 432, startDate: '2020-09-01', endDate: '2026-09-30', contractDurationYears: 6, breakoutOption: false, status: 'ACTIVE', sourceSystem: 'SAP_REFX', importedAt: '2025-01-15T08:00:00Z', lastUpdatedAt: '2025-05-10T10:00:00Z' }], schattenmarktRelease: { enabled: false } },
|
||||
],
|
||||
importedFrom: 'SAP RE-FX',
|
||||
importedAt: '2025-01-15T08:00:00Z',
|
||||
@@ -393,8 +393,8 @@ export const mockProperties: Property[] = [
|
||||
propertyNumber: 'BE-2023-011',
|
||||
description: 'Weiträumige Produktions- und Logistikhalle im Gewerbequartier Brünnen Bern. Kranbahn (5 t) in Halle West, Dreiphasenstrom 400V vorhanden. Gut angebunden an A12-Anschluss Bern-Bümpliz. Zwei Hallenabschnitte separat oder gemeinsam anmietbar. Ausbaupotenzial von 1200 m² auf dem Grundstück möglich.',
|
||||
units: [
|
||||
{ id: 'unit-011-1', propertyId: 'prop-011', floorLevel: 0, unitLabel: 'Halle West', areaSqm: 1600, available: false, rentPricePerSqm: 144, currentTenant: 'Metallbau Bern AG', leaseTerm: '11 Jahre', leaseEndDate: '2026-11-30', schattenmarktRelease: { enabled: true, availableFrom: '2026-11-30' } },
|
||||
{ id: 'unit-011-2', propertyId: 'prop-011', floorLevel: 0, unitLabel: 'Halle Ost', areaSqm: 1200, available: false, rentPricePerSqm: 144, currentTenant: 'Metallbau Bern AG', leaseTerm: '11 Jahre', leaseEndDate: '2026-11-30', schattenmarktRelease: { enabled: true, availableFrom: '2026-11-30' } },
|
||||
{ id: 'unit-011-1', propertyId: 'prop-011', floorLevel: 0, unitLabel: 'Halle West', areaSqm: 1600, available: false, rentPricePerSqm: 144, leases: [{ id: 'lease-011-1', unitId: 'unit-011-1', tenant: { id: 'ten-metallbau-011', companyName: 'Metallbau Bern AG', industry: 'Produktion & Metallverarbeitung' }, rentPerSqm: 144, startDate: '2015-11-01', endDate: '2026-11-30', contractDurationYears: 11, breakoutOption: false, status: 'ACTIVE', sourceSystem: 'SAP_REFX', importedAt: '2025-01-15T08:00:00Z', lastUpdatedAt: '2025-05-10T10:00:00Z' }], schattenmarktRelease: { enabled: true, availableFrom: '2026-11-30' } },
|
||||
{ id: 'unit-011-2', propertyId: 'prop-011', floorLevel: 0, unitLabel: 'Halle Ost', areaSqm: 1200, available: false, rentPricePerSqm: 144, leases: [{ id: 'lease-011-2', unitId: 'unit-011-2', tenant: { id: 'ten-metallbau-011', companyName: 'Metallbau Bern AG', industry: 'Produktion & Metallverarbeitung' }, rentPerSqm: 144, startDate: '2015-11-01', endDate: '2026-11-30', contractDurationYears: 11, breakoutOption: false, status: 'ACTIVE', sourceSystem: 'SAP_REFX', importedAt: '2025-01-15T08:00:00Z', lastUpdatedAt: '2025-05-10T10:00:00Z' }], schattenmarktRelease: { enabled: true, availableFrom: '2026-11-30' } },
|
||||
],
|
||||
importedFrom: 'SAP RE-FX',
|
||||
importedAt: '2025-01-15T08:00:00Z',
|
||||
|
||||
Reference in New Issue
Block a user