diff --git a/src/components/demand/NeedCardPreview.tsx b/src/components/demand/NeedCardPreview.tsx index 77efd59..eccdb51 100644 --- a/src/components/demand/NeedCardPreview.tsx +++ b/src/components/demand/NeedCardPreview.tsx @@ -4,6 +4,14 @@ import type { ParsedNeedCriteria } from '../../domain/needBuilder' import { WEIGHTING_KEYS, WEIGHTING_LABELS } from '../../domain/needBuilder' import type { WeightingKey } from '../../domain/needBuilder' +function toRaw(w: Record): Record { + const max = Math.max(...WEIGHTING_KEYS.map(k => w[k] ?? 0)) + if (max === 0) return Object.fromEntries(WEIGHTING_KEYS.map(k => [k, 3])) as Record + return Object.fromEntries( + WEIGHTING_KEYS.map(k => [k, Math.max(1, Math.round(((w[k] ?? 0) / max) * 5))]) + ) as Record +} + interface Props { criteria: ParsedNeedCriteria weights: Record @@ -21,13 +29,10 @@ const ASSET_LABELS: Record = { const CRITICAL_FIELDS = ['assetType', 'areaRange', 'preferredLocations', 'budgetRange', 'timing'] export function NeedCardPreview({ criteria: c, weights, confidenceByField, missingFields, needTitle, onNeedTitleChange }: Props) { - const maxWeight = Math.max(...WEIGHTING_KEYS.map(k => weights[k] ?? 0), 0.01) - const fieldEntries = Object.entries(confidenceByField) const overallConfidence = fieldEntries.length > 0 ? fieldEntries.reduce((sum, [, v]) => sum + v, 0) / fieldEntries.length : 0 - const lowConfidenceFields = fieldEntries.filter(([, v]) => v < 0.6).map(([k]) => k) const criticalMissing = missingFields.filter(f => CRITICAL_FIELDS.some(cf => f.toLowerCase().includes(cf.toLowerCase()))) const isLowConfidence = overallConfidence < 0.6 @@ -149,35 +154,34 @@ export function NeedCardPreview({ criteria: c, weights, confidenceByField, missi }, }} /> - {lowConfidenceFields.length > 0 && ( - - Unsichere Felder: {lowConfidenceFields.join(', ')} - - )} - {/* Weights */} - - Gewichtungsprofil - - - {WEIGHTING_KEYS.map(k => { - const pct = Math.round((weights[k] ?? 0) * 100) - return ( - - {WEIGHTING_LABELS[k]} - - {pct}% - - ) - })} - + {/* Priority groups */} + {(() => { + const rawW = toRaw(weights) + const groups = [ + { label: 'Kritisch', color: '#c0392b', keys: WEIGHTING_KEYS.filter(k => rawW[k] >= 4) }, + { label: 'Wichtig', color: '#1e3a5f', keys: WEIGHTING_KEYS.filter(k => rawW[k] === 3) }, + { label: 'Optional', color: '#94a3b8', keys: WEIGHTING_KEYS.filter(k => rawW[k] <= 2) }, + ].filter(g => g.keys.length > 0) + return ( + <> + + Prioritäten + + {groups.map(g => ( + + + + {g.label}:{' '}{g.keys.map(k => WEIGHTING_LABELS[k]).join(', ')} + + + ))} + + ) + })()} ) diff --git a/src/components/demand/NeedInput.tsx b/src/components/demand/NeedInput.tsx index 07fe2a7..ef5ed18 100644 --- a/src/components/demand/NeedInput.tsx +++ b/src/components/demand/NeedInput.tsx @@ -1,5 +1,6 @@ import { useState } from 'react' -import { Box, Card, Chip, Stack, TextField, Typography } from '@mui/material' +import { Box, Button, Card, Chip, Collapse, Stack, TextField, Typography } from '@mui/material' +import { ChevronDown, ChevronUp } from 'lucide-react' import type { ParsedNeedCriteria } from '../../domain/needBuilder' import { AssetType } from '../../domain/enums' @@ -29,6 +30,9 @@ export function NeedInput({ criteria: c, onCriteriaChange: set }: Props) { const [locationDraft, setLocationDraft] = useState('') const [mustHaveDraft, setMustHaveDraft] = useState('') + const hasDetails = !!c.budgetRange?.maxPerSqm || !!c.timing?.earliestMoveIn || (c.mustHaveCriteria?.length ?? 0) > 0 + const [showDetails, setShowDetails] = useState(hasDetails) + function addLocations(raw: string) { const tokens = raw.split(',').map(x => x.trim()).filter(Boolean) if (!tokens.length) return @@ -101,55 +105,67 @@ export function NeedInput({ criteria: c, onCriteriaChange: set }: Props) { sx={{ mb: 0.75 }} /> {(c.preferredLocations?.length ?? 0) > 0 ? ( - + {c.preferredLocations!.map(loc => ( set({ ...c, preferredLocations: c.preferredLocations!.filter(l => l !== loc) })} /> ))} - ) : } + ) : } - {/* Budget */} - Budget (max CHF/m²) - set({ ...c, budgetRange: { maxPerSqm: parseInt(e.target.value) || 0, currency: 'CHF' } })} - sx={{ width: 160, mb: 2.5 }} - slotProps={{ htmlInput: { min: 0 } }} - /> - - {/* Timing */} - Verfügbar ab - set({ ...c, timing: { earliestMoveIn: e.target.value, latestMoveIn: e.target.value, flexibleTiming: true } })} - sx={{ width: 220, mb: 2.5 }} - /> + onClick={() => setShowDetails(v => !v)} + endIcon={showDetails ? : } + sx={{ textTransform: 'none', color: '#64748b', pl: 0, mb: 1 }} + > + {showDetails ? 'Details ausblenden' : 'Budget, Timing & Must-haves'} + - {/* Must-haves */} - Must-haves - setMustHaveDraft(e.target.value)} - onKeyDown={e => { if (e.key === 'Enter' && mustHaveDraft.trim()) addMustHaves(mustHaveDraft) }} - onBlur={() => { if (mustHaveDraft.trim()) addMustHaves(mustHaveDraft) }} - sx={{ mb: 0.75 }} - /> - {(c.mustHaveCriteria?.length ?? 0) > 0 && ( - - {c.mustHaveCriteria!.map(item => ( - set({ ...c, mustHaveCriteria: c.mustHaveCriteria!.filter(m => m !== item) })} - /> - ))} - - )} + + {/* Budget */} + Budget (max CHF/m²) + set({ ...c, budgetRange: { maxPerSqm: parseInt(e.target.value) || 0, currency: 'CHF' } })} + sx={{ width: 160, mb: 2.5 }} + slotProps={{ htmlInput: { min: 0 } }} + /> + + {/* Timing */} + Verfügbar ab + set({ ...c, timing: { earliestMoveIn: e.target.value, latestMoveIn: e.target.value, flexibleTiming: true } })} + sx={{ width: 220, mb: 2.5 }} + /> + + {/* Must-haves */} + Must-haves + setMustHaveDraft(e.target.value)} + onKeyDown={e => { if (e.key === 'Enter' && mustHaveDraft.trim()) addMustHaves(mustHaveDraft) }} + onBlur={() => { if (mustHaveDraft.trim()) addMustHaves(mustHaveDraft) }} + sx={{ mb: 0.75 }} + /> + {(c.mustHaveCriteria?.length ?? 0) > 0 && ( + + {c.mustHaveCriteria!.map(item => ( + set({ ...c, mustHaveCriteria: c.mustHaveCriteria!.filter(m => m !== item) })} + /> + ))} + + )} + ) } diff --git a/src/components/demand/WeightingEditor.tsx b/src/components/demand/WeightingEditor.tsx index 828c1eb..392db52 100644 --- a/src/components/demand/WeightingEditor.tsx +++ b/src/components/demand/WeightingEditor.tsx @@ -1,5 +1,5 @@ import { useState } from 'react' -import { Box, Button, Card, Slider, Typography } from '@mui/material' +import { Box, Button, Card, Chip, Typography } from '@mui/material' import { RotateCcw } from 'lucide-react' import { WEIGHTING_KEYS, WEIGHTING_LABELS } from '../../domain/needBuilder' import type { WeightingKey } from '../../domain/needBuilder' @@ -11,7 +11,11 @@ interface Props { assetType?: string } -const IMPORTANCE_LABELS = ['', 'Unwichtig', 'Wenig wichtig', 'Wichtig', 'Sehr wichtig', 'Entscheidend'] +const PRIORITY_LEVELS = [ + { label: 'Optional', value: 1, selectedColor: '#64748b' }, + { label: 'Wichtig', value: 3, selectedColor: '#1e3a5f' }, + { label: 'Kritisch', value: 5, selectedColor: '#c0392b' }, +] as const function toRaw(w: Record): Record { const max = Math.max(...WEIGHTING_KEYS.map(k => w[k] ?? 0)) @@ -31,7 +35,7 @@ function rawToWeights(raw: Record): Record>(() => toRaw(weights)) - function handleSlider(key: WeightingKey, value: number) { + function handleSelect(key: WeightingKey, value: number) { const updated = { ...raw, [key]: value } setRaw(updated) onChange(rawToWeights(updated)) @@ -51,7 +55,7 @@ export function WeightingEditor({ weights, onChange, assetType }: Props) { Wichtigkeit der Kriterien - Schieber nach rechts = wichtiger. Gewichtung wird automatisch berechnet. + Wählen Sie für jedes Kriterium die Priorität.