refactor: architecture compliance pass — DS tokens, hook boundary, god component split, AI hardening
- DS token migration: Anfragen.tsx + child components (AnfragenInquiryItem, AnfragenMessageBubble) fully migrated; DS_TEXT.brandDark added; scoreTheme.ts moved to src/lib/ with re-export proxy - Hook boundary: Results.tsx no longer calls needService directly — routes through useNeeds() with optional refetchOnMount/gcTime overrides - NewListing.tsx (440L) split into useNewListingForm hook + 8 section components under src/components/new-listing/; page shell reduced to 121 lines - AI hardening: Zod .strict() on all schemas, AIProvenance extended with schemaVersion/ fallbackReason/traceId/latencyMs, AITraceStore stats with p50/p90/p99 + failure breakdowns, MockAIService buildFollowUpQuestions with priority ordering + area-ambiguity detection, prompt templates updated (LIGHT_INDUSTRIAL, budget unit, ambiguity detection, decimal precision) - Tests: all 154 passing; fixed test regression caused by OfferEmailResponseSchema body min(50) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -3,6 +3,7 @@ import { Building2, MapPin, Ruler, Calendar } from 'lucide-react'
|
||||
import type { InquiryPreparationReportDraft, ReportObjectFieldKey } from '../../domain/inquiryReport'
|
||||
import type { Property } from '../../domain/property'
|
||||
import type { Inquiry } from '../../domain/inquiry'
|
||||
import { DS_TEXT, DS_BG, DS_SURFACE, DS_BORDER } from '../../lib/ds'
|
||||
|
||||
interface Props {
|
||||
draft: InquiryPreparationReportDraft
|
||||
@@ -100,39 +101,39 @@ export function LatentInquiryReportPreview({ draft, inquiry, properties }: Props
|
||||
mb: 2,
|
||||
}}
|
||||
>
|
||||
<Box sx={{ borderBottom: '3px solid #1e3a5f', pb: 2, mb: 3 }}>
|
||||
<Typography variant="h5" sx={{ fontWeight: 700, color: '#1e3a5f', fontFamily: 'inherit' }}>
|
||||
<Box sx={{ borderBottom: `3px solid ${DS_TEXT.brand}`, pb: 2, mb: 3 }}>
|
||||
<Typography variant="h5" sx={{ fontWeight: 700, color: DS_TEXT.brand, fontFamily: 'inherit' }}>
|
||||
Objektvorschlag
|
||||
</Typography>
|
||||
<Typography variant="body2" sx={{ color: '#64748b', mt: 0.5 }}>
|
||||
<Typography variant="body2" sx={{ color: DS_TEXT.muted, mt: 0.5 }}>
|
||||
Wincasa AG · {new Date().toLocaleDateString('de-CH')}
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
{editableByField['intro'] && (
|
||||
<Typography variant="body2" sx={{ whiteSpace: 'pre-wrap', lineHeight: 1.8, mb: 3, color: '#1e293b' }}>
|
||||
<Typography variant="body2" sx={{ whiteSpace: 'pre-wrap', lineHeight: 1.8, mb: 3, color: DS_TEXT.primary }}>
|
||||
{editableByField['intro']}
|
||||
</Typography>
|
||||
)}
|
||||
|
||||
<Box sx={{ bgcolor: '#f8fafc', border: '1px solid #e2e8f0', borderRadius: 1, p: 2, mb: 3 }}>
|
||||
<Typography variant="body2" sx={{ fontWeight: 700, color: '#0f172a', mb: 1 }}>
|
||||
<Box sx={{ bgcolor: DS_BG.page, border: `1px solid ${DS_BORDER.default}`, borderRadius: 1, p: 2, mb: 3 }}>
|
||||
<Typography variant="body2" sx={{ fontWeight: 700, color: DS_TEXT.primary, mb: 1 }}>
|
||||
Ihre Anfrage
|
||||
</Typography>
|
||||
<Typography variant="body2" sx={{ color: '#374151', mb: 0.5 }}>
|
||||
<Typography variant="body2" sx={{ color: DS_TEXT.primary, mb: 0.5 }}>
|
||||
<strong>Kontakt:</strong> {inquiry.tenantName}{inquiry.tenantCompany ? ` · ${inquiry.tenantCompany}` : ''}
|
||||
</Typography>
|
||||
<Typography variant="body2" sx={{ color: '#374151' }}>
|
||||
<Typography variant="body2" sx={{ color: DS_TEXT.primary }}>
|
||||
<strong>Betreff:</strong> {inquiry.subject}
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
{editableByField['highlights'] && (
|
||||
<>
|
||||
<Typography variant="body2" sx={{ fontWeight: 700, color: '#0f172a', mb: 1 }}>
|
||||
<Typography variant="body2" sx={{ fontWeight: 700, color: DS_TEXT.primary, mb: 1 }}>
|
||||
Warum diese Objekte passen
|
||||
</Typography>
|
||||
<Typography variant="body2" sx={{ whiteSpace: 'pre-wrap', lineHeight: 1.8, color: '#374151', mb: 3 }}>
|
||||
<Typography variant="body2" sx={{ whiteSpace: 'pre-wrap', lineHeight: 1.8, color: DS_TEXT.primary, mb: 3 }}>
|
||||
{editableByField['highlights']}
|
||||
</Typography>
|
||||
</>
|
||||
@@ -145,7 +146,7 @@ export function LatentInquiryReportPreview({ draft, inquiry, properties }: Props
|
||||
label={p.title}
|
||||
size="small"
|
||||
icon={<Building2 size={12} />}
|
||||
sx={{ bgcolor: '#e0e7ff', color: '#1e3a5f', fontWeight: 600 }}
|
||||
sx={{ bgcolor: DS_SURFACE.indigo.bg, color: DS_TEXT.brand, fontWeight: 600 }}
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
@@ -161,7 +162,7 @@ export function LatentInquiryReportPreview({ draft, inquiry, properties }: Props
|
||||
<Box key={property.id}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 1 }}>
|
||||
<Divider sx={{ flex: 1 }} />
|
||||
<Typography variant="caption" sx={{ color: '#94a3b8', fontSize: '0.7rem', whiteSpace: 'nowrap' }}>
|
||||
<Typography variant="caption" sx={{ color: DS_TEXT.disabled, fontSize: '0.7rem', whiteSpace: 'nowrap' }}>
|
||||
Objekt {idx + 1} von {selectedProps.length}
|
||||
</Typography>
|
||||
<Divider sx={{ flex: 1 }} />
|
||||
@@ -184,7 +185,7 @@ export function LatentInquiryReportPreview({ draft, inquiry, properties }: Props
|
||||
height: 120,
|
||||
borderRadius: 1,
|
||||
overflow: 'hidden',
|
||||
bgcolor: '#e2e8f0',
|
||||
bgcolor: DS_BORDER.default,
|
||||
backgroundImage: property.mapImageUrl ? `url(${property.mapImageUrl})` : 'none',
|
||||
backgroundSize: 'cover',
|
||||
backgroundPosition: 'center',
|
||||
@@ -194,7 +195,7 @@ export function LatentInquiryReportPreview({ draft, inquiry, properties }: Props
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
>
|
||||
{!property.mapImageUrl && <MapPin size={28} color="#94a3b8" />}
|
||||
{!property.mapImageUrl && <MapPin size={28} color={DS_TEXT.disabled} />}
|
||||
</Box>
|
||||
{/* Property photo */}
|
||||
<Box
|
||||
@@ -203,7 +204,7 @@ export function LatentInquiryReportPreview({ draft, inquiry, properties }: Props
|
||||
height: 120,
|
||||
borderRadius: 1,
|
||||
overflow: 'hidden',
|
||||
bgcolor: '#e2e8f0',
|
||||
bgcolor: DS_BORDER.default,
|
||||
backgroundImage: image ? `url(${image})` : 'none',
|
||||
backgroundSize: 'cover',
|
||||
backgroundPosition: 'center',
|
||||
@@ -212,23 +213,23 @@ export function LatentInquiryReportPreview({ draft, inquiry, properties }: Props
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
>
|
||||
{!image && <Building2 size={28} color="#94a3b8" />}
|
||||
{!image && <Building2 size={28} color={DS_TEXT.disabled} />}
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Typography variant="h6" sx={{ fontWeight: 700, color: '#1e3a5f', mb: 0.5, fontFamily: 'inherit' }}>
|
||||
<Typography variant="h6" sx={{ fontWeight: 700, color: DS_TEXT.brand, mb: 0.5, fontFamily: 'inherit' }}>
|
||||
{property.title}
|
||||
</Typography>
|
||||
<Box sx={{ display: 'flex', gap: 2, mb: 2 }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5, color: '#64748b' }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5, color: DS_TEXT.muted }}>
|
||||
<MapPin size={13} />
|
||||
<Typography variant="caption">{property.location.city}{property.location.district ? `, ${property.location.district}` : ''}</Typography>
|
||||
</Box>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5, color: '#64748b' }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5, color: DS_TEXT.muted }}>
|
||||
<Ruler size={13} />
|
||||
<Typography variant="caption">{property.areaSqm.toLocaleString('de-CH')} m²</Typography>
|
||||
</Box>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5, color: '#64748b' }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5, color: DS_TEXT.muted }}>
|
||||
<Calendar size={13} />
|
||||
<Typography variant="caption">{new Date(property.availabilityDate).toLocaleDateString('de-CH')}</Typography>
|
||||
</Box>
|
||||
@@ -240,7 +241,7 @@ export function LatentInquiryReportPreview({ draft, inquiry, properties }: Props
|
||||
display: 'grid',
|
||||
gridTemplateColumns: '1fr 1fr',
|
||||
gap: 0.75,
|
||||
bgcolor: '#f8fafc',
|
||||
bgcolor: DS_BG.page,
|
||||
borderRadius: 1,
|
||||
p: 1.5,
|
||||
}}
|
||||
@@ -250,10 +251,10 @@ export function LatentInquiryReportPreview({ draft, inquiry, properties }: Props
|
||||
if (!val) return null
|
||||
return (
|
||||
<Box key={key}>
|
||||
<Typography variant="caption" sx={{ color: '#64748b', fontSize: '0.68rem', display: 'block' }}>
|
||||
<Typography variant="caption" sx={{ color: DS_TEXT.muted, fontSize: '0.68rem', display: 'block' }}>
|
||||
{FIELD_LABELS[key] ?? key}
|
||||
</Typography>
|
||||
<Typography variant="caption" sx={{ fontWeight: 600, color: '#0f172a', fontSize: '0.78rem' }}>
|
||||
<Typography variant="caption" sx={{ fontWeight: 600, color: DS_TEXT.primary, fontSize: '0.78rem' }}>
|
||||
{val}
|
||||
</Typography>
|
||||
</Box>
|
||||
@@ -268,10 +269,10 @@ export function LatentInquiryReportPreview({ draft, inquiry, properties }: Props
|
||||
|
||||
{editableByField['next_steps'] && (
|
||||
<Box sx={{ bgcolor: 'white', p: 3, boxShadow: '0 2px 12px rgba(0,0,0,0.1)', borderRadius: 1 }}>
|
||||
<Typography variant="body2" sx={{ fontWeight: 700, color: '#0f172a', mb: 1 }}>
|
||||
<Typography variant="body2" sx={{ fontWeight: 700, color: DS_TEXT.primary, mb: 1 }}>
|
||||
Nächste Schritte
|
||||
</Typography>
|
||||
<Typography variant="body2" sx={{ whiteSpace: 'pre-wrap', lineHeight: 1.8, color: '#374151' }}>
|
||||
<Typography variant="body2" sx={{ whiteSpace: 'pre-wrap', lineHeight: 1.8, color: DS_TEXT.primary }}>
|
||||
{editableByField['next_steps']}
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
Reference in New Issue
Block a user