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:
Benjamin Sutter
2026-05-24 16:10:39 +02:00
parent e36c5bc979
commit e1f4beb898
44 changed files with 1610 additions and 1058 deletions
@@ -0,0 +1,80 @@
import { Accordion, AccordionDetails, AccordionSummary, Box, Checkbox, FormControlLabel, MenuItem, TextField, Typography } from '@mui/material'
import { ChevronDown } from 'lucide-react'
import type { ParsedNeedCriteria } from '../../domain/needBuilder'
interface Props {
criteria: ParsedNeedCriteria
onChange: (c: ParsedNeedCriteria) => void
}
export function NeedExtendedRequirements({ criteria: c, onChange: set }: Props) {
return (
<Accordion disableGutters elevation={0} sx={{ border: '1px solid #e2e8f0', borderRadius: 1, mt: 1, '&:before': { display: 'none' } }}>
<AccordionSummary expandIcon={<ChevronDown size={16} />} sx={{ minHeight: 36, px: 1.5, '& .MuiAccordionSummary-content': { my: 0.5 } }}>
<Typography variant="caption" sx={{ fontWeight: 600, color: '#64748b' }}>Erweiterte Anforderungen</Typography>
</AccordionSummary>
<AccordionDetails sx={{ px: 1.5, pt: 0, pb: 1.5 }}>
<Box sx={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 0.5, mb: 1.5 }}>
<FormControlLabel
control={<Checkbox size="small" checked={c.requireGroundFloor ?? false} onChange={e => set({ ...c, requireGroundFloor: e.target.checked || undefined })} />}
label={<Typography variant="caption">Erdgeschoss erforderlich</Typography>}
/>
<FormControlLabel
control={<Checkbox size="small" checked={c.requireAirConditioning ?? false} onChange={e => set({ ...c, requireAirConditioning: e.target.checked || undefined })} />}
label={<Typography variant="caption">Klimaanlage erforderlich</Typography>}
/>
<FormControlLabel
control={<Checkbox size="small" checked={c.requireLoadingDock ?? false} onChange={e => set({ ...c, requireLoadingDock: e.target.checked || undefined })} />}
label={<Typography variant="caption">Laderampe erforderlich</Typography>}
/>
<FormControlLabel
control={<Checkbox size="small" checked={c.requireBarrierFree ?? false} onChange={e => set({ ...c, requireBarrierFree: e.target.checked || undefined })} />}
label={<Typography variant="caption">Barrierefrei erforderlich</Typography>}
/>
</Box>
<Box sx={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 1.5 }}>
<Box>
<Typography variant="caption" color="text.secondary" sx={{ display: 'block', mb: 0.5 }}>Mindest-Parkplätze</Typography>
<TextField
size="small" type="number" fullWidth placeholder="0"
value={c.requiredParkingMin ?? ''}
onChange={e => set({ ...c, requiredParkingMin: parseInt(e.target.value) || undefined })}
slotProps={{ htmlInput: { min: 0, max: 100 } }}
/>
</Box>
<Box>
<Typography variant="caption" color="text.secondary" sx={{ display: 'block', mb: 0.5 }}>Mindest-Ausbaustandard</Typography>
<TextField
select size="small" fullWidth
value={c.requiredFitOut ?? ''}
onChange={e => set({ ...c, requiredFitOut: (e.target.value as 'BASIC' | 'FULL' | 'PREMIUM') || undefined })}
>
<MenuItem value="">Kein Mindeststandard</MenuItem>
<MenuItem value="BASIC">Basisausbau</MenuItem>
<MenuItem value="FULL">Vollausbau</MenuItem>
<MenuItem value="PREMIUM">Premiumausbau</MenuItem>
</TextField>
</Box>
<Box>
<Typography variant="caption" color="text.secondary" sx={{ display: 'block', mb: 0.5 }}>Mindest-Deckenhöhe (m)</Typography>
<TextField
size="small" type="number" fullWidth placeholder="z.B. 6"
value={c.minCeilingHeightM ?? ''}
onChange={e => set({ ...c, minCeilingHeightM: parseFloat(e.target.value) || undefined })}
slotProps={{ htmlInput: { min: 2, max: 20, step: 0.5 } }}
/>
</Box>
<Box>
<Typography variant="caption" color="text.secondary" sx={{ display: 'block', mb: 0.5 }}>Mindest-Vertragslaufzeit (Monate)</Typography>
<TextField
size="small" type="number" fullWidth placeholder="z.B. 36"
value={c.minContractDurationMonths ?? ''}
onChange={e => set({ ...c, minContractDurationMonths: parseInt(e.target.value) || undefined })}
slotProps={{ htmlInput: { min: 0, max: 360, step: 12 } }}
/>
</Box>
</Box>
</AccordionDetails>
</Accordion>
)
}