36570c5bdc
- Fix MUI v9 API: PaperProps/InputLabelProps/inputProps → slotProps in 6 components - Add ExternalMarketResult alias, PropertyUnit import, OPERATIONS workspace config - Fix TradeOff.description → .concern, ScoreFactor.label → .criterion - Make schattenmarktRelease.leadTimeMonths optional, fix mock-data enum values - Fix useMatchDetailData query typing, weightingService missing WeightProfile keys - Split Pipeline/Compare/MatchDetail/IntelligenceMatchCard into sub-components - Fix all test fixtures (CreateNeedInput, CreatePropertyInput, TradeOffInput, etc.) - Add vercel.json for deployment, zero tsc errors Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
159 lines
6.3 KiB
TypeScript
159 lines
6.3 KiB
TypeScript
import { Accordion, AccordionDetails, AccordionSummary, Box, Checkbox, FormControlLabel, Typography, Button } from '@mui/material'
|
|
import { ChevronDown } from 'lucide-react'
|
|
import type { ReportObjectFieldKey, ReportObjectFieldSelection } from '../../domain/inquiryReport'
|
|
|
|
interface Props {
|
|
propertyTitle: string
|
|
value: ReportObjectFieldSelection
|
|
onChange: (v: ReportObjectFieldSelection) => void
|
|
}
|
|
|
|
type FieldGroup = {
|
|
label: string
|
|
fields: { key: ReportObjectFieldKey; label: string }[]
|
|
}
|
|
|
|
const FIELD_GROUPS: FieldGroup[] = [
|
|
{
|
|
label: 'Basisdaten',
|
|
fields: [
|
|
{ key: 'areaSqm', label: 'Fläche' },
|
|
{ key: 'rentPricePerSqm', label: 'Mietpreis' },
|
|
{ key: 'availabilityDate', label: 'Verfügbar ab' },
|
|
{ key: 'propertyNumber', label: 'Objektnummer' },
|
|
{ key: 'assetType', label: 'Asset Type' },
|
|
{ key: 'description', label: 'Beschreibung' },
|
|
],
|
|
},
|
|
{
|
|
label: 'Miet-/Vertragsdaten',
|
|
fields: [
|
|
{ key: 'leaseTerm', label: 'Mietlaufzeit' },
|
|
{ key: 'breakoutOption', label: 'Breakout-Option' },
|
|
{ key: 'breakoutOptionDate', label: 'Breakout-Option Zeitpunkt' },
|
|
{ key: 'currentTenant', label: 'Aktueller Mieter' },
|
|
{ key: 'floor', label: 'Stockwerk' },
|
|
{ key: 'parking', label: 'Parkplätze' },
|
|
{ key: 'fitOut', label: 'Ausbaugrad' },
|
|
{ key: 'isBarrierFree', label: 'Barrierefrei' },
|
|
],
|
|
},
|
|
{
|
|
label: 'Technische Hard Facts',
|
|
fields: [
|
|
{ key: 'ceilingHeightM', label: 'Raumhöhe' },
|
|
{ key: 'floorLoad', label: 'Bodenlast' },
|
|
{ key: 'loadingDocksCount', label: 'Anlieferung' },
|
|
{ key: 'goodsLift', label: 'Warenaufzug' },
|
|
{ key: 'passengerLift', label: 'Personenaufzug' },
|
|
{ key: 'powerSupplyKva', label: 'Stromanschluss (kVA)' },
|
|
{ key: 'hasServerRoom', label: 'Serverraum' },
|
|
{ key: 'internet', label: 'Internet / Konnektivität' },
|
|
{ key: 'deliveryAccess', label: 'Zufahrt / Logistik' },
|
|
],
|
|
},
|
|
{
|
|
label: 'Soft Factors',
|
|
fields: [
|
|
{ key: 'prestigeScore', label: 'Prestige' },
|
|
{ key: 'visibilityScore', label: 'Sichtbarkeit' },
|
|
{ key: 'footfallScore', label: 'Passantenfrequenz' },
|
|
{ key: 'commuterAccessScore', label: 'Pendlererreichbarkeit' },
|
|
{ key: 'talentAccessScore', label: 'Talent Access' },
|
|
{ key: 'esgScore', label: 'ESG / Nachhaltigkeit' },
|
|
{ key: 'flexibilityScore', label: 'Flexibilität' },
|
|
{ key: 'expansionPotentialScore', label: 'Expansionsmöglichkeit' },
|
|
{ key: 'taxEnvironmentScore', label: 'Steuerumfeld' },
|
|
{ key: 'publicTransportScore', label: 'ÖV-Anbindung' },
|
|
{ key: 'microLocation', label: 'Mikrostandort' },
|
|
{ key: 'competitionEnvironment', label: 'Konkurrenzumfeld' },
|
|
{ key: 'infrastructure', label: 'Infrastruktur' },
|
|
],
|
|
},
|
|
{
|
|
label: 'Marktinformationen',
|
|
fields: [
|
|
{ key: 'marketSignals', label: 'Marktsignale' },
|
|
{ key: 'negotiationHints', label: 'Verhandlungshinweise' },
|
|
{ key: 'missingData', label: 'Datenlücken / offene Punkte' },
|
|
{ key: 'dataQuality', label: 'Datenqualität' },
|
|
{ key: 'units', label: 'Stockwerkstruktur' },
|
|
],
|
|
},
|
|
]
|
|
|
|
export function ReportObjectFieldSelector({ propertyTitle, value, onChange }: Props) {
|
|
const isSelected = (key: ReportObjectFieldKey) =>
|
|
value.selectedOptionalFields.includes(key)
|
|
|
|
const toggle = (key: ReportObjectFieldKey) => {
|
|
const next = isSelected(key)
|
|
? value.selectedOptionalFields.filter(k => k !== key)
|
|
: [...value.selectedOptionalFields, key]
|
|
onChange({ ...value, selectedOptionalFields: next })
|
|
}
|
|
|
|
const selectAll = (keys: ReportObjectFieldKey[]) => {
|
|
const next = Array.from(new Set([...value.selectedOptionalFields, ...keys]))
|
|
onChange({ ...value, selectedOptionalFields: next })
|
|
}
|
|
|
|
const deselectAll = (keys: ReportObjectFieldKey[]) => {
|
|
onChange({ ...value, selectedOptionalFields: value.selectedOptionalFields.filter(k => !keys.includes(k)) })
|
|
}
|
|
|
|
return (
|
|
<Box>
|
|
<Typography variant="body2" sx={{ fontWeight: 600, color: '#0f172a', mb: 1 }}>
|
|
Felder für: {propertyTitle}
|
|
</Typography>
|
|
<Typography variant="caption" sx={{ color: '#64748b', display: 'block', mb: 1.5 }}>
|
|
Pflichtfelder (immer enthalten): Titel, Standort, Karte, Fotos
|
|
</Typography>
|
|
|
|
{FIELD_GROUPS.map(group => {
|
|
const groupKeys = group.fields.map(f => f.key)
|
|
return (
|
|
<Accordion key={group.label} disableGutters elevation={0} sx={{ border: '1px solid #e2e8f0', mb: 0.5, '&:before': { display: 'none' } }}>
|
|
<AccordionSummary expandIcon={<ChevronDown size={16} />} sx={{ minHeight: 40, '& .MuiAccordionSummary-content': { my: 0.5 } }}>
|
|
<Typography variant="body2" sx={{ fontWeight: 600, fontSize: '0.8125rem' }}>
|
|
{group.label}
|
|
</Typography>
|
|
<Typography variant="caption" sx={{ ml: 1, color: '#64748b', alignSelf: 'center' }}>
|
|
({groupKeys.filter(k => value.selectedOptionalFields.includes(k)).length}/{groupKeys.length})
|
|
</Typography>
|
|
</AccordionSummary>
|
|
<AccordionDetails sx={{ pt: 0, pb: 1 }}>
|
|
<Box sx={{ display: 'flex', gap: 1, mb: 1 }}>
|
|
<Button size="small" onClick={() => selectAll(groupKeys)} sx={{ textTransform: 'none', fontSize: '0.7rem', p: '2px 8px', minWidth: 0 }}>
|
|
Alle
|
|
</Button>
|
|
<Button size="small" onClick={() => deselectAll(groupKeys)} sx={{ textTransform: 'none', fontSize: '0.7rem', p: '2px 8px', minWidth: 0 }}>
|
|
Keine
|
|
</Button>
|
|
</Box>
|
|
<Box sx={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 0 }}>
|
|
{group.fields.map(f => (
|
|
<FormControlLabel
|
|
key={f.key}
|
|
control={
|
|
<Checkbox
|
|
size="small"
|
|
checked={isSelected(f.key)}
|
|
onChange={() => toggle(f.key)}
|
|
sx={{ py: 0.25 }}
|
|
/>
|
|
}
|
|
label={<Typography variant="caption" sx={{ fontSize: '0.75rem' }}>{f.label}</Typography>}
|
|
sx={{ m: 0 }}
|
|
/>
|
|
))}
|
|
</Box>
|
|
</AccordionDetails>
|
|
</Accordion>
|
|
)
|
|
})}
|
|
</Box>
|
|
)
|
|
}
|