feat: all 4 UX priorities — wizard, examples, onboarding, pipeline
- Offer wizard: 4→3 steps by merging 'Prüfen' into PDF review step - Search mask: quick-select chips for area ranges, budget, and city presets - Onboarding: WelcomeDialog (3 slides, localStorage) on first visit - Deal Pipeline: Kanban board (6 stages, 8 mock items, advance-to-next-stage) added to demand workspace navigation Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -17,6 +17,22 @@ const ASSET_OPTIONS = [
|
||||
{ label: 'Gemischt', value: AssetType.MIXED },
|
||||
]
|
||||
|
||||
const AREA_PRESETS = [
|
||||
{ label: '200–500 m²', min: 200, max: 500 },
|
||||
{ label: '500–1000 m²', min: 500, max: 1000 },
|
||||
{ label: '1000–2000 m²', min: 1000, max: 2000 },
|
||||
{ label: '2000–5000 m²', min: 2000, max: 5000 },
|
||||
]
|
||||
|
||||
const BUDGET_PRESETS = [
|
||||
{ label: '25 CHF/m²', value: 25 },
|
||||
{ label: '45 CHF/m²', value: 45 },
|
||||
{ label: '65 CHF/m²', value: 65 },
|
||||
{ label: '100 CHF/m²', value: 100 },
|
||||
]
|
||||
|
||||
const LOCATION_PRESETS = ['Zürich', 'Zürich-West', 'Zürich-Nord', 'Bern', 'Basel', 'Luzern']
|
||||
|
||||
function FieldLabel({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<Typography variant="caption" color="text.secondary" sx={{ fontWeight: 600, display: 'block', mb: 0.75 }}>
|
||||
@@ -70,7 +86,7 @@ export function NeedInput({ criteria: c, onCriteriaChange: set }: Props) {
|
||||
|
||||
{/* Area */}
|
||||
<FieldLabel>Fläche (m²)</FieldLabel>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 2.5 }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 0.75 }}>
|
||||
<TextField
|
||||
size="small" type="number" placeholder="Min"
|
||||
value={c.areaRange?.min || ''}
|
||||
@@ -88,35 +104,87 @@ export function NeedInput({ criteria: c, onCriteriaChange: set }: Props) {
|
||||
/>
|
||||
<Typography variant="caption" color="text.secondary">m²</Typography>
|
||||
</Box>
|
||||
<Stack direction="row" spacing={0.5} sx={{ flexWrap: 'wrap', gap: 0.5, mb: 2.5 }}>
|
||||
{AREA_PRESETS.map(p => (
|
||||
<Chip
|
||||
key={p.label}
|
||||
label={p.label}
|
||||
size="small"
|
||||
variant="outlined"
|
||||
clickable
|
||||
onClick={() => set({ ...c, areaRange: { min: p.min, max: p.max } })}
|
||||
sx={{ fontSize: '0.68rem', height: 20, color: '#64748b', borderColor: '#cbd5e1' }}
|
||||
/>
|
||||
))}
|
||||
</Stack>
|
||||
|
||||
{/* Location */}
|
||||
<FieldLabel>Standort</FieldLabel>
|
||||
<Stack direction="row" spacing={0.5} sx={{ flexWrap: 'wrap', gap: 0.5, mb: 0.75 }}>
|
||||
{LOCATION_PRESETS.map(loc => (
|
||||
<Chip
|
||||
key={loc}
|
||||
label={loc}
|
||||
size="small"
|
||||
variant={c.preferredLocations?.includes(loc) ? 'filled' : 'outlined'}
|
||||
clickable
|
||||
onClick={() => {
|
||||
const already = c.preferredLocations?.includes(loc)
|
||||
set({ ...c, preferredLocations: already
|
||||
? (c.preferredLocations ?? []).filter(l => l !== loc)
|
||||
: [...new Set([...(c.preferredLocations ?? []), loc])]
|
||||
})
|
||||
}}
|
||||
sx={c.preferredLocations?.includes(loc)
|
||||
? { fontSize: '0.68rem', height: 22, bgcolor: '#1e3a5f', color: 'white' }
|
||||
: { fontSize: '0.68rem', height: 22, color: '#64748b', borderColor: '#cbd5e1' }
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</Stack>
|
||||
<TextField
|
||||
size="small" fullWidth
|
||||
placeholder="Stadt oder Region — Enter zum Hinzufügen"
|
||||
placeholder="Weitere Stadt oder Region — Enter zum Hinzufügen"
|
||||
value={locationDraft}
|
||||
onChange={e => setLocationDraft(e.target.value)}
|
||||
onKeyDown={e => { if (e.key === 'Enter' && locationDraft.trim()) addLocations(locationDraft) }}
|
||||
onBlur={() => { if (locationDraft.trim()) addLocations(locationDraft) }}
|
||||
sx={{ mb: 0.75 }}
|
||||
/>
|
||||
{(c.preferredLocations?.length ?? 0) > 0 ? (
|
||||
{(c.preferredLocations?.filter(l => !LOCATION_PRESETS.includes(l)).length ?? 0) > 0 && (
|
||||
<Stack direction="row" spacing={0.5} sx={{ flexWrap: 'wrap', gap: 0.5, mb: 2.5 }}>
|
||||
{c.preferredLocations!.map(loc => (
|
||||
{c.preferredLocations!.filter(l => !LOCATION_PRESETS.includes(l)).map(loc => (
|
||||
<Chip key={loc} label={loc} size="small"
|
||||
onDelete={() => set({ ...c, preferredLocations: c.preferredLocations!.filter(l => l !== loc) })}
|
||||
/>
|
||||
))}
|
||||
</Stack>
|
||||
) : <Box sx={{ mb: 2.5 }} />}
|
||||
)}
|
||||
<Box sx={{ mb: (c.preferredLocations?.filter(l => !LOCATION_PRESETS.includes(l)).length ?? 0) > 0 ? 0 : 2.5 }} />
|
||||
|
||||
{/* Budget */}
|
||||
<FieldLabel>Budget (max CHF/m²)</FieldLabel>
|
||||
<Stack direction="row" spacing={0.5} sx={{ flexWrap: 'wrap', gap: 0.5, mb: 0.75 }}>
|
||||
{BUDGET_PRESETS.map(p => (
|
||||
<Chip
|
||||
key={p.label}
|
||||
label={p.label}
|
||||
size="small"
|
||||
variant={c.budgetRange?.maxPerSqm === p.value ? 'filled' : 'outlined'}
|
||||
clickable
|
||||
onClick={() => set({ ...c, budgetRange: { maxPerSqm: p.value, currency: 'CHF' } })}
|
||||
sx={c.budgetRange?.maxPerSqm === p.value
|
||||
? { fontSize: '0.68rem', height: 22, bgcolor: '#1e3a5f', color: 'white' }
|
||||
: { fontSize: '0.68rem', height: 22, color: '#64748b', borderColor: '#cbd5e1' }
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</Stack>
|
||||
<TextField
|
||||
size="small" type="number" placeholder="z.B. 45"
|
||||
size="small" type="number" placeholder="oder eigener Wert"
|
||||
value={c.budgetRange?.maxPerSqm || ''}
|
||||
onChange={e => set({ ...c, budgetRange: { maxPerSqm: parseInt(e.target.value) || 0, currency: 'CHF' } })}
|
||||
sx={{ width: 160, mb: 2.5 }}
|
||||
sx={{ width: 180, mb: 2.5 }}
|
||||
slotProps={{ htmlInput: { min: 0 } }}
|
||||
/>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user