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:
@@ -1,15 +1,29 @@
|
||||
import { Box, Button, CircularProgress, Typography } from '@mui/material'
|
||||
import { ArrowRight, Building2, Calendar, MapPin, Ruler } from 'lucide-react'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { Box, Button, CircularProgress, Divider, Typography } from '@mui/material'
|
||||
import { ArrowRight, Building2, Calendar, MapPin, Ruler, Trophy } from 'lucide-react'
|
||||
import { useNavigate } from 'react-router'
|
||||
import { usePropertyById } from '../../hooks/useProperties'
|
||||
import { matchService } from '../../services/matchService'
|
||||
import type { AdditionalPropertyMatch } from '../../domain/additionalMatch'
|
||||
|
||||
interface RelatedPropertyCardPanelProps {
|
||||
propertyId: string
|
||||
inquiryId: string
|
||||
}
|
||||
|
||||
export function RelatedPropertyCardPanel({ propertyId }: RelatedPropertyCardPanelProps) {
|
||||
export function RelatedPropertyCardPanel({ propertyId, inquiryId }: RelatedPropertyCardPanelProps) {
|
||||
const { data: property, isLoading } = usePropertyById(propertyId)
|
||||
const navigate = useNavigate()
|
||||
const [additionalMatches, setAdditionalMatches] = useState<AdditionalPropertyMatch[]>([])
|
||||
const [loadingMatches, setLoadingMatches] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
setLoadingMatches(true)
|
||||
matchService
|
||||
.getAdditionalMatchesForInquiry(inquiryId, { minScore: 80, excludePropertyId: propertyId })
|
||||
.then(setAdditionalMatches)
|
||||
.finally(() => setLoadingMatches(false))
|
||||
}, [inquiryId, propertyId])
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
@@ -102,10 +116,91 @@ export function RelatedPropertyCardPanel({ propertyId }: RelatedPropertyCardPane
|
||||
size="small"
|
||||
endIcon={<ArrowRight size={14} />}
|
||||
onClick={() => navigate('/supply/properties')}
|
||||
sx={{ textTransform: 'none', mt: 'auto' }}
|
||||
sx={{ textTransform: 'none' }}
|
||||
>
|
||||
Objekt ansehen
|
||||
</Button>
|
||||
|
||||
{/* Weitere Matches */}
|
||||
<Divider sx={{ my: 0.5 }} />
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.75 }}>
|
||||
<Trophy size={14} color="#d97706" />
|
||||
<Typography variant="caption" sx={{ color: '#64748b', fontWeight: 600, textTransform: 'uppercase', letterSpacing: 0.5, fontSize: '0.68rem' }}>
|
||||
Weitere passende Objekte
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
{loadingMatches ? (
|
||||
<CircularProgress size={16} sx={{ alignSelf: 'center' }} />
|
||||
) : additionalMatches.length === 0 ? (
|
||||
<Typography variant="caption" sx={{ color: '#94a3b8', fontSize: '0.75rem' }}>
|
||||
Keine weiteren Matches
|
||||
</Typography>
|
||||
) : (
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
|
||||
{additionalMatches.map(m => (
|
||||
<AdditionalMatchCard key={m.propertyId} match={m} />
|
||||
))}
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
function AdditionalMatchCard({ match }: { match: AdditionalPropertyMatch }) {
|
||||
const navigate = useNavigate()
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
border: '1px solid #e2e8f0',
|
||||
borderRadius: 1.5,
|
||||
overflow: 'hidden',
|
||||
bgcolor: 'white',
|
||||
}}
|
||||
>
|
||||
{match.imageUrl && (
|
||||
<Box
|
||||
sx={{
|
||||
height: 70,
|
||||
backgroundImage: `url(${match.imageUrl})`,
|
||||
backgroundSize: 'cover',
|
||||
backgroundPosition: 'center',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<Box sx={{ p: 1 }}>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', mb: 0.5 }}>
|
||||
<Typography variant="caption" sx={{ fontWeight: 600, color: '#0f172a', fontSize: '0.75rem', lineHeight: 1.3 }}>
|
||||
{match.title}
|
||||
</Typography>
|
||||
<Box
|
||||
sx={{
|
||||
px: 0.75,
|
||||
py: 0.125,
|
||||
borderRadius: 1,
|
||||
bgcolor: match.matchScore >= 90 ? '#fef3c7' : '#e0e7ff',
|
||||
color: match.matchScore >= 90 ? '#92400e' : '#3730a3',
|
||||
fontWeight: 700,
|
||||
fontSize: '0.65rem',
|
||||
flexShrink: 0,
|
||||
ml: 0.5,
|
||||
}}
|
||||
>
|
||||
{match.matchScore}%
|
||||
</Box>
|
||||
</Box>
|
||||
<Typography variant="caption" sx={{ color: '#64748b', fontSize: '0.7rem', display: 'block' }}>
|
||||
{match.location} · {match.areaSqm.toLocaleString('de-CH')} m²
|
||||
</Typography>
|
||||
<Button
|
||||
size="small"
|
||||
endIcon={<ArrowRight size={11} />}
|
||||
onClick={() => navigate('/supply/properties')}
|
||||
sx={{ textTransform: 'none', fontSize: '0.7rem', mt: 0.5, p: 0, minWidth: 0, color: '#1e3a5f' }}
|
||||
>
|
||||
Ansehen
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user