e169f8e310
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>
132 lines
4.9 KiB
TypeScript
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>
|
|
)
|
|
}
|