feat: Anfragencenter Parts A–E — read/unread, prep wizard, offer flow, report preview
- Part A: Replace InquiryStatus badges with WhatsApp-style unread indicators (isRead, unreadCount, lastReadAt on Inquiry; markThreadAsRead in service + hook) - Part B: RelatedPropertyCardPanel — reposition "Objekt ansehen", add "Weitere Matches" section via matchService.getAdditionalMatchesForInquiry - Part C: PreparationWizard 4-step dialog — property selection, report generation progress, field editing + ReportObjectFieldSelector, PDF preview + finalize - Part D: OfferCreationWizard 4-step dialog triggered from first tenant message — data capture, field editing, viewing appointments, PDF generation + attach to reply - Part E: LatentInquiryReportPreview (2-page A4 doc), ReportObjectFieldSelector (5 accordion groups, 35+ optional fields), mapImageUrl on Property domain Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,150 @@
|
||||
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: 'Grunddaten',
|
||||
fields: [
|
||||
{ key: 'areaSqm', label: 'Fläche (m²)' },
|
||||
{ key: 'rentPricePerSqm', label: 'Mietpreis (CHF/m²/Jahr)' },
|
||||
{ key: 'availabilityDate', label: 'Verfügbar ab' },
|
||||
{ key: 'leaseTerm', label: 'Mietlaufzeit' },
|
||||
{ key: 'breakoutOption', label: 'Breakout-Option' },
|
||||
{ key: 'currentTenant', label: 'Aktueller Mieter' },
|
||||
{ key: 'propertyNumber', label: 'Objektnummer' },
|
||||
{ key: 'assetType', label: 'Objekttyp' },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Technische Merkmale',
|
||||
fields: [
|
||||
{ key: 'floor', label: 'Stockwerk' },
|
||||
{ key: 'parking', label: 'Parkplätze' },
|
||||
{ key: 'ceilingHeightM', label: 'Deckenhöhe (m)' },
|
||||
{ key: 'loadingDocksCount', label: 'Laderampen' },
|
||||
{ key: 'powerSupplyKva', label: 'Stromanschluss (kVA)' },
|
||||
{ key: 'hasServerRoom', label: 'Serverraum' },
|
||||
{ key: 'isBarrierFree', label: 'Barrierefrei' },
|
||||
{ key: 'fitOut', label: 'Ausbaustandard' },
|
||||
{ key: 'publicTransportScore', label: 'ÖV-Score' },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Weiche Faktoren',
|
||||
fields: [
|
||||
{ key: 'prestigeScore', label: 'Prestige' },
|
||||
{ key: 'visibilityScore', label: 'Sichtbarkeit' },
|
||||
{ key: 'footfallScore', label: 'Passantenfrequenz' },
|
||||
{ key: 'commuterAccessScore', label: 'Pendleranbindung' },
|
||||
{ key: 'talentAccessScore', label: 'Talentpool' },
|
||||
{ key: 'esgScore', label: 'ESG-Bewertung' },
|
||||
{ key: 'flexibilityScore', label: 'Flexibilität' },
|
||||
{ key: 'expansionPotentialScore', label: 'Expansionspotenzial' },
|
||||
{ key: 'taxEnvironmentScore', label: 'Steuerumgebung' },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Beschreibung & Medien',
|
||||
fields: [
|
||||
{ key: 'description', label: 'Beschreibung' },
|
||||
{ key: 'units', label: 'Stockwerkstruktur' },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Datenqualität',
|
||||
fields: [
|
||||
{ key: 'dataQuality', label: 'Datenqualität' },
|
||||
{ key: 'softFactors', label: 'Weiche Faktoren (Gesamt)' },
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
const MANDATORY: ReportObjectFieldKey[] = ['title', 'location', 'mapImageUrl', 'images']
|
||||
|
||||
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)
|
||||
const allSelected = groupKeys.every(k => value.selectedOptionalFields.includes(k))
|
||||
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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user