d15a13e485
- Delete all ops page components (ReviewQueue, AIMonitoring, Governance, SourceMonitoring, ActivityTimeline, SignalPipeline) - Remove OPERATIONS workspace from AppShell config, nav order, path detection - Remove all /ops/* routes from App.tsx - Remove WorkspaceType.OPERATIONS from allowedWorkspaces in authService, sessionStore, permissions - Keep MarketIntelligence page (already moved to /supply/market-intelligence) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
156 lines
6.1 KiB
TypeScript
156 lines
6.1 KiB
TypeScript
import { useState } from 'react'
|
||
import { Box, Card, Chip, Stack, TextField, Typography } from '@mui/material'
|
||
import type { ParsedNeedCriteria } from '../../domain/needBuilder'
|
||
import { AssetType } from '../../domain/enums'
|
||
|
||
interface Props {
|
||
criteria: ParsedNeedCriteria
|
||
onCriteriaChange: (c: ParsedNeedCriteria) => void
|
||
}
|
||
|
||
const ASSET_OPTIONS = [
|
||
{ label: 'Büro', value: AssetType.OFFICE },
|
||
{ label: 'Retail', value: AssetType.RETAIL },
|
||
{ label: 'Logistik', value: AssetType.LOGISTICS },
|
||
{ label: 'Produktion', value: AssetType.PRODUCTION },
|
||
{ label: 'Light Industrial', value: AssetType.LIGHT_INDUSTRIAL },
|
||
{ label: 'Gemischt', value: AssetType.MIXED },
|
||
]
|
||
|
||
function FieldLabel({ children }: { children: React.ReactNode }) {
|
||
return (
|
||
<Typography variant="caption" color="text.secondary" sx={{ fontWeight: 600, display: 'block', mb: 0.75 }}>
|
||
{children}
|
||
</Typography>
|
||
)
|
||
}
|
||
|
||
export function NeedInput({ criteria: c, onCriteriaChange: set }: Props) {
|
||
const [locationDraft, setLocationDraft] = useState('')
|
||
const [mustHaveDraft, setMustHaveDraft] = useState('')
|
||
|
||
function addLocations(raw: string) {
|
||
const tokens = raw.split(',').map(x => x.trim()).filter(Boolean)
|
||
if (!tokens.length) return
|
||
set({ ...c, preferredLocations: [...new Set([...(c.preferredLocations ?? []), ...tokens])] })
|
||
setLocationDraft('')
|
||
}
|
||
|
||
function addMustHaves(raw: string) {
|
||
const tokens = raw.split(',').map(x => x.trim()).filter(Boolean)
|
||
if (!tokens.length) return
|
||
set({ ...c, mustHaveCriteria: [...new Set([...(c.mustHaveCriteria ?? []), ...tokens])] })
|
||
setMustHaveDraft('')
|
||
}
|
||
|
||
return (
|
||
<Card elevation={0} sx={{ p: 3, border: '1px solid #e2e8f0', height: '100%' }}>
|
||
<Typography variant="subtitle1" sx={{ fontWeight: 700, mb: 0.5 }}>Kriterien verfeinern</Typography>
|
||
<Typography variant="caption" color="text.secondary" sx={{ display: 'block', mb: 2.5 }}>
|
||
Ergänzen oder korrigieren Sie die extrahierten Felder.
|
||
</Typography>
|
||
|
||
{/* Asset Type */}
|
||
<FieldLabel>Nutzungstyp</FieldLabel>
|
||
<Stack direction="row" spacing={0.5} sx={{ flexWrap: 'wrap', gap: 0.5, mb: 2.5 }}>
|
||
{ASSET_OPTIONS.map(opt => (
|
||
<Chip
|
||
key={opt.value}
|
||
label={opt.label}
|
||
size="small"
|
||
variant={c.assetType === opt.value ? 'filled' : 'outlined'}
|
||
clickable
|
||
onClick={() => set({ ...c, assetType: c.assetType === opt.value ? undefined : opt.value })}
|
||
sx={c.assetType === opt.value
|
||
? { bgcolor: '#1e3a5f', color: 'white', '& .MuiChip-label': { color: 'white' } }
|
||
: {}}
|
||
/>
|
||
))}
|
||
</Stack>
|
||
|
||
{/* Area */}
|
||
<FieldLabel>Fläche (m²)</FieldLabel>
|
||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 2.5 }}>
|
||
<TextField
|
||
size="small" type="number" placeholder="Min"
|
||
value={c.areaRange?.min || ''}
|
||
onChange={e => set({ ...c, areaRange: { min: parseInt(e.target.value) || 0, max: c.areaRange?.max ?? 0 } })}
|
||
sx={{ width: 100 }}
|
||
slotProps={{ htmlInput: { min: 0 } }}
|
||
/>
|
||
<Typography variant="body2" color="text.secondary">–</Typography>
|
||
<TextField
|
||
size="small" type="number" placeholder="Max"
|
||
value={c.areaRange?.max || ''}
|
||
onChange={e => set({ ...c, areaRange: { min: c.areaRange?.min ?? 0, max: parseInt(e.target.value) || 0 } })}
|
||
sx={{ width: 100 }}
|
||
slotProps={{ htmlInput: { min: 0 } }}
|
||
/>
|
||
<Typography variant="caption" color="text.secondary">m²</Typography>
|
||
</Box>
|
||
|
||
{/* Location */}
|
||
<FieldLabel>Standort</FieldLabel>
|
||
<TextField
|
||
size="small" fullWidth
|
||
placeholder="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 ? (
|
||
<Stack direction="row" spacing={0.5} sx={{ flexWrap: 'wrap', gap: 0.5, mb: 2.5 }}>
|
||
{c.preferredLocations!.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 }} />}
|
||
|
||
{/* Budget */}
|
||
<FieldLabel>Budget (max CHF/m²)</FieldLabel>
|
||
<TextField
|
||
size="small" type="number" placeholder="z.B. 45"
|
||
value={c.budgetRange?.maxPerSqm || ''}
|
||
onChange={e => set({ ...c, budgetRange: { maxPerSqm: parseInt(e.target.value) || 0, currency: 'CHF' } })}
|
||
sx={{ width: 160, mb: 2.5 }}
|
||
slotProps={{ htmlInput: { min: 0 } }}
|
||
/>
|
||
|
||
{/* Timing */}
|
||
<FieldLabel>Verfügbar ab</FieldLabel>
|
||
<TextField
|
||
size="small"
|
||
placeholder="z.B. Q3 2025 oder 01.09.2025"
|
||
value={c.timing?.earliestMoveIn ?? ''}
|
||
onChange={e => set({ ...c, timing: { earliestMoveIn: e.target.value, latestMoveIn: e.target.value, flexibleTiming: true } })}
|
||
sx={{ width: 220, mb: 2.5 }}
|
||
/>
|
||
|
||
{/* Must-haves */}
|
||
<FieldLabel>Must-haves</FieldLabel>
|
||
<TextField
|
||
size="small" fullWidth
|
||
placeholder="z.B. ÖV-Anbindung, Parkplätze — Enter zum Hinzufügen"
|
||
value={mustHaveDraft}
|
||
onChange={e => 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 && (
|
||
<Stack direction="row" spacing={0.5} sx={{ flexWrap: 'wrap', gap: 0.5 }}>
|
||
{c.mustHaveCriteria!.map(item => (
|
||
<Chip key={item} label={item} size="small"
|
||
onDelete={() => set({ ...c, mustHaveCriteria: c.mustHaveCriteria!.filter(m => m !== item) })}
|
||
/>
|
||
))}
|
||
</Stack>
|
||
)}
|
||
</Card>
|
||
)
|
||
}
|