Files
property-match/src/pages/supply/NewListing.tsx
T
Benjamin Sutter e169f8e310 feat(matching): annuity-based fit-out cost in score + fix unapplied DQ/confidence modifiers
Mieterausbau / fit-out economics — surface true total cost of occupancy:
- Annuity calc (annuityFactor + effectiveAnnualBurdenPerSqm) replaces straight-line ÷5; FITOUT_ANNUITY_RATE=5%
- New hardFacts.fitOutByLandlord: "Wer baut aus?" toggle in NewListing — landlord-borne fit-out is priced into rent (no surcharge), tenant-borne SHELL/BASIC adds annuitized cost minus MAB
- scoreBudget now compares effective annual burden (rent + fit-out annuity) vs budget instead of cold rent only; FULL/PREMIUM and landlord-borne unchanged
- FitOutCostPanel + CompareTableBody compute annuitized, tenant-aware burden
- Central FIT_OUT_LABELS with industry/international vocabulary (Rohbau·Core&Shell, Edelrohbau·CAT A, etc.)
- Activate existing generateFitOutAdvice via useFitOutAdvice hook + new FitOutAdvicePanel (MIETERAUSBAU/BKZ/MAB-Amortisation + negotiation tip), shown for tenant-borne SHELL/BASIC
- MAB field only asked when tenant builds out (optional) — one new toggle, no extra data burden for property managers

Fix: DQ/confidence modifiers were computed but never applied to finalScore (hardcoded 0 in output) — now folded into rawFinal and exposed. Trust-first: weak data quality lowers the score. Resolves 3 pre-existing red tests.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-20 18:19:58 +02:00

132 lines
4.9 KiB
TypeScript

import { Alert, Box, Button, CircularProgress, Divider, Typography } from '@mui/material'
import { ArrowLeft } from 'lucide-react'
import { useLocation, useNavigate } from 'react-router'
import { useNewListingForm } from '../../hooks/useNewListingForm'
import {
AiAssistCard,
AreaDetailsSection,
AddressSection,
SoftFactorsSection,
TechnicalDetailsSection,
ImageUrlSection,
FloorPlanUrlSection,
ContactSection,
CreatedScreen,
} from '../../components/new-listing'
import { DS_TEXT } from '../../lib/ds'
import type { LocationState } from './newListingConstants'
export default function NewListing() {
const navigate = useNavigate()
const { state } = useLocation() as { state: LocationState | null }
const pre = state?.prefill ?? {}
const form = useNewListingForm(pre)
if (form.created) {
return (
<CreatedScreen
onViewListings={() => navigate('/supply/my-listings')}
onCreateAnother={form.resetForm}
/>
)
}
return (
<Box sx={{ maxWidth: 760, mx: 'auto', px: 3, py: 4 }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1.5, mb: 3 }}>
<Button
variant="text"
size="small"
startIcon={<ArrowLeft size={16} />}
onClick={() => navigate(-1)}
sx={{ color: DS_TEXT.muted, textTransform: 'none', px: 0 }}
>
Zurück
</Button>
</Box>
<Typography sx={{ fontWeight: 700, fontSize: '1.125rem', lineHeight: 1.3, color: '#0f172a', mb: 0.5 }}>Neues Inserat erstellen</Typography>
<Typography variant="body2" color="text.secondary" sx={{ mb: 3 }}>
{form.isPrefilled
? `Einheit ${pre.unitLabel ?? ''} aus Portfolio vorausgefüllt — Angaben prüfen und veröffentlichen.`
: 'Fläche direkt inserieren — ohne vollständiges Objekt im Portfolio.'}
</Typography>
<AiAssistCard
text={form.aiText}
onTextChange={form.setAiText}
onParse={form.handleAiParse}
parsing={form.aiParsing}
applied={form.aiApplied}
/>
<AreaDetailsSection
assetType={form.assetType} onAssetTypeChange={form.setAssetType}
areaSqm={form.areaSqm} onAreaSqmChange={form.setAreaSqm}
rentPerSqm={form.rentPerSqm} onRentPerSqmChange={form.setRentPerSqm}
availableFrom={form.availableFrom} onAvailableFromChange={form.setAvailableFrom}
description={form.description} onDescriptionChange={form.setDescription}
/>
<AddressSection
street={form.street} onStreetChange={form.setStreet}
houseNumber={form.houseNumber} onHouseNumberChange={form.setHouseNumber}
postalCode={form.postalCode} onPostalCodeChange={form.setPostalCode}
city={form.city} onCityChange={form.setCity}
/>
<SoftFactorsSection softLevels={form.softLevels} onChange={form.setSoftLevel} />
<TechnicalDetailsSection
floor={form.floor} onFloorChange={form.setFloor}
fitOut={form.fitOut} onFitOutChange={form.setFitOut}
fitOutByLandlord={form.fitOutByLandlord} onFitOutByLandlordChange={form.setFitOutByLandlord}
parking={form.parking} onParkingChange={form.setParking}
ceilingHeight={form.ceilingHeight} onCeilingHeightChange={form.setCeilingHeight}
mieterausbaubeitrag={form.mieterausbaubeitrag} onMieterausbaubeitragChange={form.setMieterausbaubeitrag}
isFlexible={form.isFlexible} onIsFlexibleChange={form.setIsFlexible}
minLettableSqm={form.minLettableSqm} onMinLettableSqmChange={form.setMinLettableSqm}
/>
<ImageUrlSection
images={form.images}
imageInput={form.imageInput}
onImageInputChange={form.setImageInput}
onAdd={form.addImage}
onRemove={form.removeImage}
/>
<FloorPlanUrlSection
floorPlanUrl={form.floorPlanUrl}
onFloorPlanUrlChange={form.setFloorPlanUrl}
/>
<ContactSection
name={form.contactName} onNameChange={form.setContactName}
email={form.contactEmail} onEmailChange={form.setContactEmail}
phone={form.contactPhone} onPhoneChange={form.setContactPhone}
/>
{form.error && <Alert severity="error" sx={{ mb: 2 }}>{form.error}</Alert>}
<Divider sx={{ mb: 2 }} />
<Box sx={{ display: 'flex', justifyContent: 'flex-end', gap: 2 }}>
<Button variant="outlined" onClick={() => navigate(-1)} disabled={form.submitting}>
Abbrechen
</Button>
<Button
variant="contained"
sx={{ bgcolor: DS_TEXT.brand, '&:hover': { bgcolor: DS_TEXT.brandDark } }}
onClick={form.handleSubmit}
disabled={form.submitting}
startIcon={form.submitting ? <CircularProgress size={14} color="inherit" /> : null}
>
{form.submitting ? 'Wird erstellt…' : 'Inserat veröffentlichen'}
</Button>
</Box>
</Box>
)
}